Original-URL des Artikels: https://www.golem.de/news/julia-eine-programmiersprache-nicht-nur-fuer-die-wissenschaft-1906-142107.html    Veröffentlicht: 25.06.2019 12:02    Kurz-URL: https://glm.io/142107

Julia

Eine Programmiersprache nicht nur für die Wissenschaft

Die noch relativ junge Programmiersprache Julia erzeugt flotte Binärprogramme und kommt vor allem bei der Verarbeitung von großen Datenmengen zum Einsatz. Sie glänzt mit einer einfachen Syntax und lässt sich auch außerhalb der Wissenschaft sehr gut einsetzen.

Die Programmiersprache Julia wird seit sieben Jahren Open Source entwickelt. Sie eignet sich durch ihre speziellen Eigenschaften hervorragend für die Verarbeitung großer Datenmengen und für wissenschaftliche Anwendungen. Ähnlich wie C, C++ und Python ist Julia aber auch als General Purpose Language ausgelegt. Mit ihr lassen sich also Anwendungen für fast alle Zwecke schreiben, so dass die Sprache auch außerhalb der Nische Wissenschaft gut aufgehoben ist.

In Julia geschriebene Programme laufen ähnlich schnell wie übersetzte C-Programme. Im Hintergrund nutzt Julia dafür das LLVM-Framework, das den eigentlichen nativen Programmcode erst zur Laufzeit erstellt (Just-in-Time, JIT). Die Referenzimplementierung aller Werkzeuge und Bibliotheken steht unter der freizügigen MIT-Lizenz.

Leicht verständliche Syntax

Die Syntax erinnert leicht an eine Mischung aus Lisp und Python. Julia unterstützt neben einer funktionalen auch eine rudimentäre objektorientierte Programmierung. Dabei setzt die Sprache stark auf das sogenannte Multiple-Dispatch-Konzept: Entwickler dürfen mehrere Funktionen mit dem gleichen Namen, aber unterschiedlichen Argumenten definieren. Julia wählt dann bei einem Aufruf automatisch die am besten passende Funktion.

Die Standardbibliothek umfasst unter anderem Funktionen zur Parallelverarbeitung, Netzwerkkommunikation und Profiling. Abschließend unterstützt Julia ähnlich wie Lisp die Metaprogrammierung, über die sich der Quellcode vor der Ausführung selbst modifizieren kann.

Analog zu vielen Skriptsprachen besitzt auch Julia einen interaktiven Interpreter, der den eingegebenen Quellcode direkt ausführt. Der Interpreter lässt sich mit Code aus Dateien und über die Standardeingabe füttern.

Einfache mathematische Notation

Julia bietet eine dynamische Typisierung, dank derer eine Variable beliebige Inhalte aufnehmen kann:

x = 5 x = Hallo Welt!

Die Variablennamen dürfen aus Unicode-Zeichen bestehen. Julia macht sich die Unicode-Unterstützung ebenfalls zunutze. So kann man einfach √4 schreiben, um die Wurzel aus 4 zu ziehen. Julia ermittelt den Typ einer Variablen automatisch im Hintergrund. Bei Bedarf kann man ihn aber auch mit dem Operator :: an Ausdrücken und Variablen explizit vorgeben.

Im Fall von x::UInt64 interpretiert Julia die Variable x als 64 Bit große, vorzeichenlose Integerzahl. Hängt der Operator an einem Ausdruck, prüft Julia, ob der zurückgelieferte Wert dem Typ entspricht. Im folgenden Beispiel muss das Ergebnis von 2+3 eine Integerzahl sein, andernfalls erzeugt Julia einen Fehler:

y = (2+3)::Int

Einer Variablen darf man direkt eine Zahl voranstellen, wie etwa im Ausdruck 2x+3. Julia führt dann automatisch eine Multiplikation mit dem Variableninhalt durch. Des Weiteren lassen sich Vergleichsoperatoren wie < oder > verketten:

1 < 2 < 3 == 3 > 2 >= 1

Dieser Ausdruck ist in den Augen von Julia true. Dank dieser Konventionen lassen sich recht elegant mathematische Formeln notieren.

Analog zu UInt64 und Int kennt Julia viele weitere Integer- und Gleitkommatypen, komplexe und rationale Zahlen und natürlich boolesche Werte. Zudem bindet Julia die GNU Multiple Precision Arithmetic Library (GMP) und die GNU MPFR Library ein, welche über die Typen BigInt und BigFloat eine effiziente Langzahlarithmetik ermöglichen.

Strings lassen sich per *-Operator schnell verketten. Umgekehrt kann man über entsprechende Indexangaben ein Zeichen oder einen Teil ausschneiden. Im folgenden Beispiel würde y den Text lo W enthalten:

x = Hallo * Welt! y = x[4:7]

Dank der eingebundenen PCRE-Bibliothek wertet Julia sogar Perl-kompatible reguläre Ausdrücke aus. Wie in Perl kann man zudem Variableninhalte in Strings einsetzen:

name = Tim x = Hallo $name !



Funktionen und gute Argumente



Wie in vielen Skriptsprachen lassen sich Funktionen recht kompakt in Julia notieren:

function f(x,y) x + y end

Dazu gibt es obendrein die Kurzschreibweise f(x,y) = x + y. Julia liefert standardmäßig den Wert des letzten Ausdrucks in der Funktion zurück. Im Zweifelsfall kann man mit return einen bestimmten Wert zurückgeben. Mehrere Rückgabewerte sind ebenfalls möglich, die Julia in ein entsprechendes Tupel packt und dieses dann zurückgibt.

Tupel ergänzen die schon aus anderen Sprachen bekannten Arrays und können beliebige Werte speichern: x = (2.3, Hallo, 5+4). Der Zugriff erfolgt mit der Klammernotation, x[2] liefert beispielsweise Hallo.

Funktionen behandelt Julia als First-Class-Objekte. Sie lassen sich folglich Variablen zuweisen, als Argumente übergeben und als Rückgabewerte zurückliefern. Möglich sind auch anonyme Funktionen:

function (x) 2x+3 end

Für sie gibt es ebenfalls eine Kurzschreibweise, die für das Beispiel x -> 3x+2 lautet. Wie auch in anderen Sprachen werden anonyme Funktionen hauptsächlich dafür genutzt, um sie direkt als Argumente an eine andere Funktion zu übergeben.

Gute Argumente

Die Argumente von Funktionen lassen sich mit Standard-Werten vorbelegen: function f(a, b=2) . Damit würde es genügen, f(1) aufzurufen. Ergänzend darf man den Argumenten Namen geben. Dies hebt die Bedeutung hervor, zudem darf man so ihre Reihenfolge vertauschen. Diese sogenannten Keywords Arguments stehen hinter einem Semikolon:

function male(x, y; pinsel=fein, dicke=1) ... male(1,2, dicke=2)

Auf Wunsch kann eine Funktion auch beliebig viele Argumente entgegennehmen, die Julia dann innerhalb der Funktion in einem Tupel verfügbar macht (Varargs Functions). Auch die Operatoren wie +, * oder - sind für Julia allesamt Funktionen - 1+2 ist nur eine alternative Schreibweise für +(1,2).

Immer im Fluss

Bedingte Anweisungen erfolgen mit dem aus vielen anderen Sprachen bekannten if-else-Konstrukt, wobei die Bedingung immer true oder false sein muss:

if x > 0 Positive Zahl else Negative Zahl end

Julia bietet nur eine while- und eine for-Schleife. Letztgenannte iteriert über eine Zahlenfolge oder die Elemente in einem Container. In folgendem Beispiel gibt sie die Zahlen von 1 bis 5 aus:

for i = 1:5 println(i) end

Schleifen lassen sich mit break abbrechen, continue startet umgehend den nächsten Schleifendurchlauf. Zwei verschachtelte Schleifen können Entwickler zu einer zusammenziehen: for i = 1:2, j = 3:4 println((i, j)) end

Julia kennt Exceptions, die Funktionen bei einem Fehler mit throw werfen können. Ähnlich wie in Java fängt die Umgebung die Fehler über einen try- und catch-Block ab.

Typisch

Julia kennt unter anderem die Integertypen Int8, Int16 und Int32. Hin und wieder möchte man jedoch Code schreiben, der mit allen Integertypen umgehen kann. Dabei helfen die Abstract Types, die zusammen eine Hierarchie, den Type Graph, bilden. Ein Abstract Type ist beispielsweise der Typ Number, der wiederum für alle Zahlen steht. Die Funktion foo(a::Numbers) würde folglich alle Zahlen als Argument akzeptieren.

Am Ende der Hierarchie stehen die Primitive Types, die einfach eine vorgegebene Anzahl Bits im Speicher belegen. Bei Bedarf kann man Abstract Types und Primitive Types selbst definieren. In folgendem Beispiel ist der abstrakte Datentyp MeineZahl ein Subtyp von Number abstract type MeineZahl <: Number end

Ergänzend hält Julia noch weitere interessante Datentyp-Konzepte bereit. So lassen sich mit Union{} mehrere Typen in einem vereinen und so etwa der Datentyp StringoderZahl umsetzen. Den Typ können Entwickler zudem im Programmcode abfragen und vergleichen. Beispielsweise prüft isa(), ob ein Objekt einem ganz bestimmten Typ entspricht - isa(1, Int) wäre beispielsweise true.



Multiple Dispatch

In Julia dürfen mehrere Funktionen mit dem gleichen Namen, aber unterschiedlichen Argumenten existieren. Bei einem Aufruf der Funktion wählt dann Julia automatisch die passende aus. Im folgenden Beispiel gibt es gleich zwei Mal eine Funktion foo(): function foo(a::Numbers) ... function foo(a::Float16) ...

Beide führen eine Berechnung mit einer Zahl a durch. Ruft man später foo() mit einer Zahl vom Typ Float16 auf, würde Julia selbstständig zur zweiten Funktion greifen, in allen anderen Fällen die erste wählen. Auf diesem Weg realisiert die Programmiersprache Polymorphie.

Die einzelnen Varianten von foo() bezeichnet Julia als Methoden. Der Begriff ist somit etwas anders ausgelegt als in objektorientierten Sprachen, wo Methoden in der Regel Teil einer Klasse sind. Die Auswahl der passenden Methode bezeichnet man allgemein als Dispatch.

Julia orientiert sich dabei immer an allen übergebenen Argumenten und deren Typen sowie dem Type Graph. Bei objektorientierten Sprachen fällt die Wahl hingegen meist anhand der Klasse, zu der das Objekt gehört. Julias Ansatz bezeichnet man daher auch als Multiple Dispatch. Dieser ist vor allem bei mathematischen Aufgaben hilfreich. Das bekannteste Beispiel ist die Funktion +, die auf allen möglichen Datentypen eine Addition umsetzen muss. In Julia dürfen Entwickler + einfach um passende Varianten für ihre eigenen Datentypen ergänzen. Es lassen sich sogar Regeln vorgeben, nach denen Julia gemischte Typen (wie die Addition einer Ganzzahl mit einer Gleitkommazahl) in einen eigenen Typ überführen soll (Promotion).

Klassenersatz

Anders als objektorientierte Sprachen kennt Julia keine Klassen. Ähnlich wie C lassen sich jedoch mit dem Schlüsselwort struct mehrere Variablen zu einem neuen Datentyp zusammenfassen:

struct Person name alter::UInt8 end

Julia nennt diese zusammengesetzten Datentypen Composite Types. Von der im Beispiel entstandenen Person lässt sich mit einem Funktionsaufruf ein Objekt erzeugen:

hans = Person(Hans Meier, 35)

hans ist damit vom Typ Person. Die aufgerufene Funktion bezeichnet Julia als Constructor. Ähnlich wie das gleichnamige Pendant in objektorientierten Sprachen kann man auch hier den von Julia automatisch bereitgestellten Contructor überladen, indem man einfach weitere passende Methoden definiert.

Standardmäßig lassen sich die Elemente von hans über den Punktoperator zwar auslesen (a=hans.alter), aber nicht verändern. Das gelingt erst, wenn man der struct noch explizit das Schlüsselwort mutable voranstellt. Die entsprechenden Objekte verwaltet Julia grundsätzlich auf dem Heap.

Generisch

Composite Types lassen sich parametrisieren. Andere Sprachen kennen das Konzept als Generics. Die wiederum sind beispielsweise nützlich, wenn der eigentlich zu nutzende Datentyp noch unbekannt ist:

struct Point{T} x::T y::T end a = Point{UInt8}(2,3)

Erst bei der Erstellung eines Objekts entscheidet sich, welche Daten "Point" speichert. Im Beispiel ist der Typ von a anschließend Point{UInt8}. Analog lassen sich auch Funktionen parametrisieren.

Parallelverarbeitung

Julia kann mehrere Funktionen parallel ausführen. Diese sogenannten Tasks lassen sich pausieren und später fortsetzen. Daten tauschen die Tasks über sogenannte Channel aus. Dabei handelt es sich um First-In-First-Out-(FIFO-)Queues, in die mehrere Tasks gleichzeitig Daten schreiben und lesen können:

function count(c::Channel) for zahl=1:10 put!(c, zahl) end end; chnl = Channel(count); a=take!(chnl) println(a)

count ist der Task, der via put!() den entsprechenden Wert in den Channel c schiebt. Nach jedem put!() hält Julia die Funktion count() an und übergibt die Kontrolle wieder dem aufrufenden Code. Der Channel-Construktor Channel(count) erstellt einen Task, der an den Channel gebunden ist. take!() liest ein Datum aus dem Channel und sorgt dafür, dass der producer() weiterläuft.

Intern realisiert Julia die Tasks als Coroutinen. Ergänzend bietet die Version 1.1 der Programmiersprache auch experimentelle Unterstützung für Multithreading. Stabil ist hingegen schon die Unterstützung für verteiltes Rechnen. Dabei läuft der Julia-Interpreter mehrfach lokal oder auf entfernten Rechnern. Der Programmierer kann dann gezielt auf den entfernten Rechnern einzelne Julia-Funktionen aufrufen.

Metaprogrammierung

Ähnlich wie in Lisp kann sich der Julia-Programmcode selbst verändern. Dazu stellt die Programmiersprache passende Funktionen bereit. Hinzu kommen die an Lisp angelehnten Makros. Diese Helfer nehmen mehrere Argumente entgegen, die sie in einen Ausdruck einbauen:

macro hallo(x) return :( println(Hallo , $x) ) end @hallo(Paul)

@hallo ersetzt Julia gegen println(Hallo , Paul). Den von Makro vorgegebenen Code baut Julia zusammen, noch bevor das komplette Programm startet. Auch bei Makros greift wieder das Mulitple-Dispatch-Prinzip, es kann folglich mehrere Methodendefinitionen geben.

Viele weitere Kleinigkeiten

Neben den vorgestellten Konstrukten bietet Julia noch viele weitere interessante Konzepte. So lassen sich etwa Arrays extrem flexibel über sogenannte Comprehensions erstellen. Wer die Mengendefinition aus der Mathematik kennt, dürfte mit der Syntax schnell warm werden. Das folgende Beispiel liefert das Array [4,5]:

x = [a+b for a=1:2, b=3]

Diese Notation kann man auch in einer sogenannten Generator Expression verwenden. sum(n for n=1:10) berechnet etwa die Summe aller Zahlen von 1 bis 10.

Besonders für Matrixrechnungen interessant ist die Funktion broadcast(), die eine ihr übergebene Funktion auf alle Elemente eines Arrays anwendet. Sofern notwendig, passt die Funktion dabei die Dimensionen der beteiligten Arrays an, ohne dabei zusätzlichen Speicher zu belegen.

Des Weiteren bietet Julia ein eingebautes Dokumentationssystem: Platziert man einen String alleine vor eine Funktionsdefinition oder anderen entsprechenden Codestellen, interpretiert sie Julia als Dokumentation (sogenannte Docstrings). Diese Hilfetexte können unter anderem die Kommandozeilenfassung von Julia sowie die Juno-IDE auswerten und durchsuchen.

Abschließend lässt sich Julia-Code über entsprechende Schnittstellen aus C-Programmen aufrufen. Umgekehrt können Programmierer aus dem Julia-Code heraus externe Programme starten sowie C- und Fortran-Code aufrufen. Auf diese Weise lassen sich zahlreiche C-Bibliotheken einbinden und weiternutzen.

Julia kombiniert die Vorteile einer Skriptsprache mit effizient und flott laufenden Binärdateien. Insbesondere bei mathematischen Berechnungen, der Verarbeitung von Daten und in der Wissenschaft bietet sie eine interessante Alternative zu Python, R und Matlab. Wer in Julia einsteigen möchte, sollte die offizielle Dokumentation konsultieren. Sie bespricht äußerst ausführlich alle Aspekte der Sprache und hält zahlreiche Tipps für die tägliche Arbeit und den Aufbau von Julia-Programmen bereit.  (tsh)


Verwandte Artikel:
Libuv: I/O-Bibliothek für Node.js und andere wird stabil   
(21.11.2014, https://glm.io/110719 )
Ada und Spark: Mehr Sicherheit durch bessere Programmiersprachen   
(11.06.2019, https://glm.io/141211 )
Javascript: NPM-Führung geht gegen Arbeitnehmerklagen vor   
(14.06.2019, https://glm.io/141906 )
Code-Hoster: Github löscht Account ohne weitere Informationen   
(07.06.2019, https://glm.io/141766 )
Ghidra: Coreboot nutzt NSA-Tool zum Reverse Engineering   
(06.06.2019, https://glm.io/141746 )

© 1997–2019 Golem.de, https://www.golem.de/