Die noch junge Programmiersprache Julia verspricht, die Flexibilität interpretierter Sprachen wie Python mit der Geschwindigkeit von C zu vereinen.
Python und C gehören nicht nur unter RasPi-Nutzern zu den beliebten Programmiersprachen. Julia tritt mit dem Anspruch an, den Komfort und die Flexibilität dynamisch typisierter und interpretierter Sprachen wie Python oder Ruby mit dem Tempo kompilierter Sprachen wie C zu vereinen. Die Sprache spezialisiert sich dabei auf daten- und rechenintensive Aufgaben sowie das Bearbeiten mathematischer Fragestellungen.
Aktuelle Julia-Versionen laufen problemlos unter 32- und 64-Bit-Linux auf dem PC. Auch der Einsatz unter Raspbian auf dem Raspberry Pi stellt kein Problem dar. Dort gibt es auch Bibliotheken zum Ansprechen der GPIO, darüber hinaus geht etwa das Parallelisieren von Algorithmen auf den vier Kernen aktueller RasPi-Modelle vergleichsweise einfach vonstatten. Grund genug, einen Blick auf die noch junge Programmiersprache zu werfen.
Gestatten: Julia
Seit der Veröffentlichung der ersten Version im Jahr 2012 hat sich die Sprache sehr dynamisch weiterentwickelt. Version 1.0 erschien erst im Herbst 2018. Alle Tutorials, die sich auf frühere Versionen beziehen, sollten Sie mit Vorsicht genießen. Die offizielle Dokumentation [2] halten die Entwickler zwar stets aktuell, sie umfasst aber herausfordernde 1276 Seiten und bringt Entscheidendes nicht immer gut auf den Punkt.
Dieser Artikel bietet einen niedrigschwelligen Einstieg in die Arbeit mit Julia – mit dem Blick für das Wesentliche und dem notwendigen Mut zur Lücke. Er setzt allerdings die Kenntnis grundlegender Begriffe der Software-Entwicklung voraus.
Installation
Alle gängigen Distributionen führen Julia in ihren Repositories, allerdings nur in älteren Versionen der sich rasch entwickelnden Programmiersprache. Eine Ausnahme machen da nur topaktuelle Releases: Unter Ubuntu 20.04 LTS etwa installieren Sie Julia über das Paketmanagement mit dem Aufruf sudo apt install julia und erhalten dann die zum Stand April 2020 aktuelle Version 1.4.1.
Die in diesem Beitrag gezeigten Beispiele sollten zwar auch mit älteren Julia-Versionen funktionieren, bei weitergehenden Julia-Exkursionen empfiehlt sich aber der Einsatz einer aktuellen Variante. Auf der Projektseite [1] finden Sie die Binärdateien der Mitte April 2020 veröffentlichten Version 1.4.1 für alle gängigen Betriebssysteme und Prozessorarchitekturen. Laden Sie die Variante Generic Linux Binaries for x86 je nach Bedarf in der Version für 32- oder 64-Bit-Linux herunter (in Listing 1, Zeile 1 ist es die 64-Bit-Variante) und entpacken Sie sie nach ~/bin/ (Zeile 2). Damit der Befehl julia im Terminal funktioniert, setzen Sie einen symbolischen Link (Zeile 3).
Listing 1
$ wget https://julialang-s3.julialang.org/bin/linux/x64/1.4/julia-1.4.1-linux-x86_64.tar.gz $ tar -xvzf julia-1.3.1-linux-armv7l.tar.gz -C ~/bin $ ln -s ~/bin/julia-1.3.1/bin/julia ~/bin/julia
Interaktiver Modus
Mit dem Befehl julia starten Sie nun den Interpreter im Terminal im interaktiven Modus (Abbildung 1). So können Sie einzelne Ausdrücke und Codefragmente ohne den Umweg über eine Datei ausprobieren und mit der Sprache experimentieren. Per Tabulator rufen Sie die Autovervollständigung auf.
Drücken Sie auf einer leeren Zeile die Tabulatortaste, liefert der Interpreter eine Liste aller geladenen Funktionen und Schlüsselwörter zurück. Mit der Eingabe eines Fragezeichens öffnen Sie die Hilfe. Geben Sie Ausdrücke direkt ein, erhalten Sie unmittelbar das Ergebnis (Listing 2). Mit exit() verlassen Sie den Interpreter.
Listing 2
julia> 1 + 1 2 julia> 7 * 8 56 julia> 2 ^ 4 16
Die Division zweier ganzer Zahlen mittels Rechenzeichen ergibt eine Fließkommazahl. Die Funktion div() erledigt eine ganzzahlige Division. Für den Rest nach Division zeichnen der Modulo-Operator % und die Funktion mod() zuständig (Listing 3).
Listing 3
julia> 10 / 2 5.0 julia> div(10, 2) 5 julia> mod(10, 3) 1
Numerische Datentypen
Die Funktion typeof() liefert den Typ eines Werts. Die 32-Bit-Version von Julia interpretiert ganzzahlige Literale als 32-Bit-Zahlen, sofern sie in diesen Wertebereich hineinpassen. Die Funktionen typemin() und typemax() geben Auskunft über die Grenzen des Wertebereichs jedes numerischen Datentyps oder eines seiner Vertreter. Die letzte Zeile in Listing 4 zeigt, dass Julia einen Überlauf nicht abfängt.
Listing 4
julia> typeof(3.14) Float64 julia> typeof(7) Int32 julia> typemax(7) 2147483647 julia> typeof(2147483648) Int64 julia> typemax(7) + 1 -2147483648
Andererseits gilt bei mathematischen Operationen mit Operanden unterschiedlichen Typs: Das Resultat ist immer vom Typ mit der höheren Genauigkeit beziehungsweise dem größeren Wertebereich (Listing 5).
Listing 5
julia> typeof(3 + 4.0) Float64 julia> typeof(2147483648 + 1) Int64
Für ganze Zahlen stehen Typen mit 8, 16, 32, 64 und 128 Bit bereit, jeweils mit oder ohne Vorzeichen. Vorzeichenlose Typen führen ein vorgestelltes U für “unsigned”, also etwa UInt8. Julia zeigt sie als hexadezimale Zahlen an (Listing 6). Für Gleitkommazahlen bietet Julia die Typen Float16, Float32 und Float64 an. Die Typen BigInt und BigFloat ermöglichen eine für den normalen Alltag praktisch unbegrenzte Genauigkeit.
Listing 6
julia> typemax(UInt8) 0xff julia> typemax(UInt16) 0xffff
Variablen und Konstruktoren
Julia ist wie Python eine dynamisch typisierte Sprache; die explizite Angabe des Typs einer Variablen ist nicht notwendig. Die Programmiersprache folgert den Typ aus dem zugewiesenen Ausdruck. Im Beispiel aus Listing 7 unterdrückt ein Semikolon die Ausgabe von Zwischenergebnissen.
Listing 7
julia> a = 1; julia> b = 2; julia> c = a + b 3
Möchten Sie explizit mit einem bestimmten Datentyp rechnen, erzeugen Sie Werte per Aufruf mit dem Konstruktor. Diese Konstruktoren eignen sich in vielen Fällen zum Umwandeln von Datentypen (Listing 8).
Listing 8
julia> unsig = UInt8(1) 0x01 julia> sig = Int8(unsig) 1
Dateien ausführen
Dateien mit Julia-Code speichern Sie mit der Endung .jl ab. Das Ausführen geschieht dann über das Kommando julia code.jl im Terminal. In dem Fall müssen Sie – anders als im interaktiven Modus – Ausgaben explizit vorgeben, etwa mit print() oder println().
Auf weitere Argumente auf der Kommandozeile neben dem Pfad oder Dateinamen greifen Sie über das Array ARGS im Programm zu. Listing 9 zeigt Code, der mittels einer For-Schleife sämtliche Argumente zeilenweise ausgibt.
Listing 9
for arg in ARGS println(arg) end
Im Workshop kam der Editor Nano zum Bearbeiten der Dateien zum Einsatz. Das Syntax-Highlighting richten Sie ein, indem Sie die online verfügbare Datei julia.nanorc [3] ins Verzeichnis ~/.nanorc kopieren (Abbildung 2).

Abbildung 2: Nach der Integration der Datei julia.nanorc zeigt Nano eine entsprechende Datei mit farbiger Syntax an.
Mittels der Funktion include("code.jl") binden Sie Codedateien in andere Files ein oder führen sie im interaktiven Modus aus. Das Verarbeiten von Argumenten auf der Kommandozeile sieht Julia dabei nicht vor. Beim Ausführen per include() im interaktiven Modus gibt der Interpreter das Ergebnis des letzten Ausdrucks im Terminal aus. Die in der eingebundenen Datei getroffenen Definitionen hält er aber im Speicher.
Kontrollstrukturen
Listing 10 zeigt weitere Beispiele für die Anwendung von For-Schleifen. Die erste gibt die Zahlen von 1 bis 3 aus, die zweite läuft von 0 bis 30 in Zehnerschritten, die dritte geht in Viertelschritten von 0 nach 1.
Listing 10
for i in 1:3 println(i) end for i in 0:10:30 println(i) end for f in 0:0.25:1 println(f) end
Der Typ Bool speichert Wahrheitswerte, die Literale schreiben sich true und false. Es gelten die üblichen Operatoren für Logik und Vergleich. Allerdings sind im Unterschied etwa zu Python, Ruby oder C lediglich boolesche Ausdrücke wahrheitsfähig. if 10 ... oder Ähnliches erzeugt also eine Fehlermeldung. Listing 11 zeigt die Anwendung boolescher Ausdrücke in einer If-Konstruktion.
Listing 11
for i in -3:3
if i < 2
println(i, " ist kleiner 2!")
elseif i > 2
println(i, " ist größer 2!")
else
println(i, " ist gleich 2!")
end
end
Listing 12 demonstriert eine While-Schleife in Aktion. In Zeile 3 sehen Sie auch, wie Julia Nutzereingaben mit readline() abfragt. Da die Eingabe zunächst als String vorliegt, erfordert die weitere Verarbeitung das Umwandeln in eine ganze Zahl.
Listing 12
while true
println("Bitte gib eine Zahl ein, die durch 7 teilbar ist!")
x = readline()
if tryparse(Int32, x) == nothing
println("Das war keine Zahl!")
continue
end
x = parse(Int32, x)
if mod(x,7) == 0
println(x, " ist durch 7 teilbar!")
break
end
end
Ob das mit dem jeweiligen String überhaupt funktioniert, prüft das Skript mittels der Funktion tryparse() in Zeile 4. Falls nicht, gibt es eine Warnung aus und führt mit continue den aktuellen Durchgang fort. Ist die Eingabe vom Typ Int32, schreibt die Funktion parse() in Zeile 8 den Wert in die Variable x. Zeile 9 prüft, ob es möglich ist, den Wert ohne Rest durch 7 zu teilen. Gelingt das, sorgt break für das Verlassen der While-Schleife und damit für das Ende des Programms.
Funktionen definieren
Julia bietet drei verschiedene Möglichkeiten, um Funktionen zu definieren. Listing 13 demonstriert diese drei Schreibweisen für eine identisch arbeitende Funktion, die das Argument x mit 3 multiplizieren soll. Die erste Variante bietet sich an, wenn der Funktionskörper nur aus einer Zeile besteht. Sie macht sehr knappen und gut lesbaren Code möglich. Die zweite empfiehlt sich bei mehrzeiligen Funktionskörpern. Auf den Nutzen der dritten, anonymen Variante kommen wir später zurück.
Listing 13
f1(x) = x*3 function f2(x) x*3 end f3 = x -> 3*x println(f1(3)) #> 9 println(f2(7)) #> 21 println(f3(9)) #> 27
Für Funktionen gilt: Das Ergebnis des zuletzt ausgewerteten Ausdrucks entspricht dem Rückgabewert der Funktion; das Schlüsselwort return ist optional. In manchen Situationen hilft es, explizit aus der Funktion herauszuspringen. Für Funktionen, die nichts zurückliefern sollen, schreiben Sie return nothing.
Typisierung von Funktionen
Optional gestattet Julia das Typisieren von Funktionen. Die Variante aus Listing 14 akzeptiert lediglich Argumente vom Typ Int32; einen Funktionsaufruf mit einer Fließkommazahl als Argument quittiert Julia dann mit einer Fehlermeldung. Diese weicht in einer aktuellen Version von Julia von der gezeigten ab.
Listing 14
julia> f(x::Int32) = 3*x f (generic function with 1 method) julia> f(3.0) ERROR: MethodError: no method matching f(::Float64) [...]
Die Begriffe Funktion und Methode haben in Julia sehr präzise Bedeutungen, die deutlich von denen in anderen Programmiersprachen abweichen. Funktion meint den Bezeichner einer Funktion. Eine Methode hingegen meint die konkrete Implementation einer Funktion auf Argumente eines bestimmten Typs. Definieren Sie in der Sitzung aus Listing 14 f() auch noch für ein Argument vom Typ Float32, erhält die Funktion f() zwei Methoden (Listing 15).
Listing 15
[...]
julia> f(x::Float32) = 3*x
f (generic function with 2 methods)
Die Funktion methods() gibt Auskunft über die Anzahl und Signatur der zu einer Funktion gehörenden Methoden (Listing 16).
Listing 16
julia> methods(f)
# 2 methods for generic function "f":
[1] f(x::Float32) in Main at REPL[2]:1
[2] f(x::Int32) in Main at REPL[1]:1
Auch + ist eine Funktion, wie der Ausdruck julia> +(1, 2) verdeutlicht. Sie besitzt insgesamt 166 Methoden, die Sie mit julia> methods(+) auflisten. Möchten Sie den Typ des Rückgabewerts einer Funktion angeben, tippen Sie julia> f(x::Int32)::Int32 = 3*x.
Anders als bei dem von Java oder C++ bekannten Überladen entscheidet Julia nicht beim Kompilieren, welche Methode zum Zuge kommt, sondern zur Laufzeit. Dieses Verfahren heißt Multiple Dispatch (Mehrfachverteilung).
Methoden können sich dabei genauso auf konkrete wie auf abstrakte Typen und Mischungen von beiden beziehen. Ein abstrakter Typ ist zum Beispiel Signed, von dem alle ganzzahligen Typen mit Vorzeichen abgeleitet sind. Der abstrakte Typ Integer dient als Supertyp aller Ganzzahlen (Listing 17). Mit supertype() bringen Sie den direkten Vorfahren eines Typs in Erfahrung. Der Typ Any ist der Supertyp aller Typen.
Listing 17
julia> supertype(Int64) Signed julia> supertype(Int8) Signed julia> supertype(Signed) Integer
Listing 18 definiert neun verschiedene Methoden für eine Funktion g() und zeigt einige Beispiele für Methodenaufrufe. Versuchen Sie nachzuvollziehen, welche Methode für welche Argumente gültig ist.
Listing 18
g(x::String) = println("String")
g(x::Float32) = println("Float32")
g(x::Unsigned) = println("Unsigned")
g(x::Integer) = println("Integer")
g(x::AbstractFloat) = println("AbstractFloat")
g(x::Real) = println("Real")
g(x::Any, y::Float32) = println("Any, Float32")
g(x::Float32, y::Any) = println("Float32, Any")
g(x::Any, y::Any) = println("Any, Any")
g(10)
#> Integer
g(UInt(64))
#> Unsigned
g("hallo")
#> String
g(22.0)
#> AbstractFloat
g(7, 8.9)
#> Any, Any
g(7, Float32(8.9))
#> Any, Float32
Komplexere Funktionen
Listing 19 definiert mit make_adder() eine Funktion, die Funktionen mittels anonymer Definition erzeugt und als Rückgabewert zurückliefert. In den Zeilen 3 und 4 liefert make_adder() die Funktionen add7() und add12(). Die Zeilen 6 und 8 zeigen die Anwendung.
Zeile 11 speichert ein Array unter dem Namen arr. Die Funktion map() erwartet als erstes Argument eine anonyme Funktion oder einen Funktionsnamen und als zweites ein Array oder einen Vertreter eines anderen aufzählbaren Typs. Die Zeilen 13 und 16 zeigen die Anwendung von map() mit den zur Laufzeit erzeugten Funktionen und (in Zeile 19) die Anwendung mit einer anonymen Funktion.
Listing 19
make_adder(val) = x -> x+val add7 = make_adder(7) add12 = make_adder(12) add7(3) #> 10 add12(12) #> 24 arr = [1, 2, 3] map(add7, arr) #> [8, 9, 10] map(add12, arr) #> [13, 14, 15] map(x -> 2*x, arr) #> [2, 4, 6]
Königsdisziplin Arrays
Der flexible Umgang mit Arrays beliebiger Dimension gehört zu den starken Seiten von Julia. Die Funktionen zum Erzeugen und Modifizieren von Arrays böten Stoff für einen eigenen Artikel. Listing 19 stellte bereits die einfache Literal-Schreibweise für Arrays vor. Wenn Sie sie im interaktiven Modus verwenden, erhalten Sie ein Ergebnis wie in den Zeilen 1 bis 5 von Listing 20.
Listing 20
julia> [1,2,3] 3-element Array{Int32,1}: 1 2 3 julia> ["x", 1] 2-element Array{Any,1}: "x" 1 julia> [7, 8, 9][1] 7
An der Ausgabe sehen Sie, dass der Datentyp Array parametrisch ist. Der Inhalt der geschweiften Klammer gibt darüber Auskunft, dass dieses Array nur Elemente vom Typ Int32 enthält und eindimensional ist.
In den Zeilen 6 bis 9 von Listing 20 hat das Array den Typ Any, da es keinen anderen Datentyp gibt, der die Typen String und Int32 zusammenfasst. Es versteht sich fast von selbst: Wenn es um das flotte Bearbeiten großer Datenmengen geht, sollten Sie nach Möglichkeit mit homogenen Arrays und Typen fester Länge arbeiten.
Sie sprechen Elemente wie in vielen anderen Programmiersprachen über einen Index an, den Sie in eckigen Klammern schreiben. Der gewichtige Unterschied: Bei Julia besitzt das erste Element den Index 1 (Listing 20, Zeile 10).
Initialisieren
Listing 21 zeigt einige Varianten zum Initialisieren von Arrays. Die Zeilen 1 und 5 erzeugen Arrays per Listen-Abstraktion. Für die String-Darstellung eindimensionaler Arrays funktioniert println() gut, bei mehreren Dimensionen fallen die Ergebnisse von display() aber oft leserlicher aus.
Listing 21
arr = [x for x in 1:3];
println(arr)
#> [1, 2, 3]
arr = [10* x + y for x in 1:3, y in 1:3];
display(arr)
#> 3×3 Array{Int32,2}:
#> 11 12 13
#> 21 22 23
#> 31 32 33
zeros(3)
#> [0.0, 0.0, 0.0]
ones(3)
#> [1.0, 1.0, 1.0]
zeros(Int32, 3)
#> [0, 0, 0]
zeros(UInt8, 2, 2, 2)
#> 2×2×2 Array{UInt8,3}:
#> [:, :, 1] =
#> 0x00 0x00
#> 0x00 0x00
#>
#> [:, :, 2] =
#> 0x00 0x00
#> 0x00 0x00
rand()
#> 0.395879...
rand(Bool)
#> false
rand(UInt8)
#> 0xad
rand(Int8, 2, 2, 2)
#> 2×2×2 Array{Int8,3}:
#> [:, :, 1] =
#> -117 59
#> -84 -101
#>
#>[:, :, 2] =
#> -80 93
#> -64 -19
Die Funktionen zeros() und ones() erzeugen mit Nullen oder Einsen gefüllte Arrays. Mit dem ersten Argument spezifizieren Sie den Typ. Hat das erste Argument keinen Typ, füllt der Interpreter das Array mit Werten vom Typ Float64.
Die nachfolgenden Argumente bestimmen die Anzahl der Dimensionen und deren Umfang. Der Befehl zeros(UInt8, 2, 2, 2) in Zeile 19 liefert ein dreidimensionales Array, bei dem jede Dimension eine Ausdehnung von zwei Elementen vom Typ UInt8 besitzt. Alle Elemente haben den Wert 0, also 0x00. Das Beispiel zeigt, wie Julia Arrays mit Dimensionen größer 2 per display() darstellt, nämlich scheibchenweise.
Die Funktion rand() (ab Zeile 29) liefert ohne Argument eine Zufallszahl zwischen 0 und 1 vom Typ Float64. Wie bei zeros() und ones() legt ein optionales erstes Argument den Typ des Rückgabewerts fest. Bei numerischen Typen, die keine Fließkommazahlen darstellen, wählt rand() aus dem kompletten Wertebereich des jeweiligen Typs aus. Sprich: rand(Int8) gibt Werte zwischen -128 und 127 zurück.
Auch für rand() gilt: Nachfolgende ganzzahlige Argumente bestimmen Dimension und Ausdehnung. Das demonstriert Zeile 36 aus Listing 21.
Punkt-Operator
Listing 22 zeigt, wie Sie Funktionen mit einem Argument auf Arrays beliebiger Dimensionalität anwenden: Schreiben Sie statt f(arr) einfach f.(arr) und schon multipliziert in diesem Fall Julia jedes Element des zweidimensionalen Arrays mit 10.
Listing 22
f(x) = x*10
arr = rand(Int8, 2, 2)
#> 2×2 Array{Int8,2}:
#> -21 79
#> 102 40
arr = f.(arr)
#> 2×2 Array{Int32,2}:
#> -210 790
#> 1020 400
Zu guter Letzt zeigt Listing 23 einige Möglichkeiten, auf Elemente von Arrays zuzugreifen und Arrays zu modifizieren. Eine knappe, aber gehaltvolle Übersicht zu Julia im Allgemeinen und Array-Funktionen im Besonderen bietet die Kurzreferenz “The Julia Express” von Bogumil Kaminski [4].
Listing 23
arr = [10*x+y for x in 1:3, y in 1:3]
3×3 Array{Int32,2}:
11 12 13
21 22 23
31 32 33
# Zugriff
arr[1] #> 11
arr[9] #> 33
arr[2, 2] #> 22
# Ausschneiden
arr[1:2, 1:2]
#> 2×2 Array{Int32,2}:
#> 11 12
#> 21 99
# Zuweisung
arr[2, 2] = 99
# Einfügen
arr = [1, 2, 3]
push!(arr, 40)
#> [1, 2, 3, 40]
Fazit und Ausblick
Trotz der vielen Beispiele konnten wir hier einige fundamentale Datentypen wie String, Dict, Tupel oder Rational nur am Rande oder gar nicht ansprechen. Auch gewährt der Artikel nur einen grundlegenden Einblick in die Methodik der Programmiersprache, wobei sich aber trotzdem zeigt, welches Potenzial in ihr schlummert. Dem Autor gefielen die eleganten Methoden zum Initialisieren von Arrays und der dynamische Umgang mit Funktionen und Funktionsaufrufen. Wir hoffen, dass es Ihnen ähnlich ergeht und Sie interessiert sind, mehr über Julia zu erfahren. (tle)
Der Autor
Pit Noack ist ein freiberuflicher Medienkünstler, Autor und Dozent, dem es Spaß macht, Programmiersprachen anzutesten.
Infos
-
Julia-Binaries: https://julialang.org/downloads/
-
Handbuch (englisch): https://docs.julialang.org/en/v1/
-
Syntax-Highlighting für Nano: https://github.com/Naereen/nanorc/blob/master/julia.nanorc
-
Kurzreferenz “The Julia Express”: http://bogumilkaminski.pl/files/julia_express.pdf






