Einführung in Julia

13 Minuten zum Lesen

Die Sprachelemente von Julia lassen sich intuitiv verstehen und sind ausführlich in docs.julialang.org dokumentiert. Es gibt eine ausführliche Einführung in Wikibooks.

Für einen ersten Eindruck werden hier einige grundlegende Konzepte vorgestellt. Durch die Beispiele wird deutlich, wie sich knapp und dennoch leicht lesbar leistungsfähige Anweisungsfolgen in Julia formulieren lassen.

REPL – Eingabe von Kommandos

REPL steht für „read eval print loop“. Wird Julia aufgerufen, erscheint folgendes Bild mit einem Prompt oder Eingabeaufforderung.

repl

Julia nimmt Kommandos entgegen. Die Kommandos werden ausgeführt, sobald Julia erkennt, dass die Eingabe abgeschlossen ist. Das Ergebnis wird direkt ausgegeben. Es stehen mathematische Operatoren, mathematische Funktionen, mathematische Konstanten und Funktionen für Zeichenketten zur Verfügung.

julia> 7 + 8 * 3 + -1
30

julia> sin(0.5 * π) # Eingabe [\pi TAB]
1.0

julia>  # Eingabe [\euler]
 = 2.7182818284590...

julia> exp(2.5)
12.182493960703473

julia> Base.MathConstants.e^2.5
12.182493960703473

julia> findfirst("c", "Joachim")
4:4

julia> string("Joachim", Char(32), "Ringelnatz")
"Joachim Ringelnatz"

julia> "Joachim" * " " * "Ringelnatz"
"Joachim Ringelnatz"

julia> "Xxoo--ooxxX" ^ 7
"Xxoo--ooxxXXxoo--ooxxXXxoo--ooxxXXxoo--ooxxXXxoo--ooxxXXxoo--ooxxXXxoo--ooxxX"

julia> length(ans)
77

Der Verkettungsoperator für Zeichenketten ist also * und nicht +! Entsprechend erhält man mit ^ eine mehrfache Verkettung.

Julia erlaubt es, mit Brüchen und komplexen Zahlen zu rechnen.

julia> 1//3 + 1//2
5//6

julia> ans * 0.1
0.08333333333333334

julia> 1 / 3//8
8//3

julia> (-2 + im) * (2 + im)
-5 + 0im

julia> exp(0 + 3.0im)
-0.9899924966004454 + 0.1411200080598672im

Verkettung

Das Ergebnis einer Operation lässt sich mit dem Verkettungsoperator |> direkt als Eingabe für eine weitere Operation verwenden.

julia> (3 + 5) * 0.3π |> sin
0.9510565162951535

Historie

Häufig braucht man die gleichen Kommandos mehrfach oder möchte sie in geänderter Form wieder aufrufen. Hier ist die Historie der REPL sehr hilfreich. Mit den Pfeiltasten der Tastatur kann man durch die Historie wandern. Die letzten Kommandos erscheinen am Prompt und können verändert oder mit Enter nocheinmal aufgerufen werden. Gibt man den Beginn eines Kommandos ein, kann man mit den Pfeiltasten in der Historie nach Kommandos suchen, die mit den eingegebenen Zeichen beginnen.

Die Auswerteschleife, REPL, erkennt den Prompt, so dass Beispiele wie diese direkt in das Fenster des Julia-Prompts kopiert werden können.

Hilfe durch den Zugang zur Dokumentation

Die Dokumention einer Funktion kann man sich mit einem Fragezeichen ? ausgeben lassen. Der Prompt wechselt dann zu help?>.

help?> length
search: length

  length(s::AbstractString)

  The number of characters in string s.

     Example
    ≡≡≡≡≡≡≡≡≡

  julia> length("jμΛIα")
  5

Gibt man das Fragezeichen zweimal ein, erhält man einen Hinweis auf die komplette Dokumentation und auf Einführungen (Tutorials).

Typen

Werte haben jeweils einen Typ, wie Number, Real, Int, Int32, Int64, Float32, Float64, String, usw.. Zwischen Typen können Teilmengenbeziehungen gelten, z.B. Int32 <: Real <: Number. Ein Typ, der eine Obermenge bezeichnet, ist immer ein abstrakter Typ, d.h. es können keine Objekte dieses Typs erzeugt werden.

Es gibt den besonderen Typ Any, der alle anderen Typen in sich vereint, d.h. jeder andere Typ bezeichnet eine Teilmenge von Any. Umgekehrt bezeichnet Union{} die leere Menge des Typsystems. Die Dokumentation widmet dem Typsystem ein eigenes Kapitel. Es ist wichtig für das Verständnis für die Art und Weise, wie Julia Funktionen, bzw. Methoden aufruft.

julia> typeof(3)
Int64

julia> typeof(3f0)
Float32

julia> typeof(3.0)
Float64

julia> typeof("drei")
String

julia> typeof(3//1)
Rational{Int64}

julia> typeof(3 + 0im)
Complex{Int64}

Variablen

Das jeweils letzte Ergebnis wird in der Variablen ans abgelegt, so dass man mit ihm weiterrechnen kann.

Es können eigene Variablen mit Werten belegt werden. Wenn eine Zahl und eine Variable nebeneinander stehen, wird * als Operator angenommen. Ein Strichpunkt ; beendet eine Anweisung braucht aber nicht am Ende einer Zeile zu stehen. Schließt eine Zeil aber dennoch mit ; ab, gibt Julia kein Ergebnis auf dem Terminal aus.

Variablennamen können Zeichen aus dem Unicode-Zeichensatz enthalten. Julia kümmert sich darum, Speicher bereitzustellen und wieder freizugeben. Variablen können nacheinander unterschiedliche Werte und auch Werte unterschiedlichen Typs annehmen.

julia> fläche = 120.75
120.75

julia> x = 7.3; y = 8.53;

julia> 3x + 0.5y
26.165

julia> x = "Grenzwert"
"Grenzwert"

julia> typeof(x)
String

julia> α = 7.5; Γ = 17.8;

julia> α * Γ
133.5

Kontrollstrukturen

Blöcke werden durch die Schlüsselwörter begin und end gekennzeichnet. Verzweigungen und Schleifen werden mit if, for, while eingeleitet, und die entsprechenden Blöcke können mit break verlassen oder mit continue im nächsten Durchgang forgesetzt werden. Das folgende Beispiel zeigt auch, wie Zahlenwerte durch Interpolation mit $ in einen String eingebettet werden können.

julia> i = 1881
1881

julia> pa = 4; ra = 3; pb = 100; rb = 19;

julia> while i < 1930
           global i += 1; global ra -= 1; global rb -= 1
           if ra == 0
               ra = pa
               if rb == 0
                   rb = pb
                   continue
               end
               println("Schaltjahr: $i")
           end
       end
Schaltjahr: 1884
Schaltjahr: 1888
Schaltjahr: 1892
Schaltjahr: 1896
Schaltjahr: 1904
Schaltjahr: 1908
Schaltjahr: 1912
Schaltjahr: 1916
Schaltjahr: 1920
Schaltjahr: 1924
Schaltjahr: 1928

Einrückungen dienen nur der besseren Lesbarkeit. Für den Julia-Parser spielen sie keine Rolle.

Tupel und Arrays

Mit runden Klammern (, ) erzeugt man ein Tupel, d.h. eine Anordnung von Elementen, die in einem bestimmten Tupel nicht verändert werden können. Die Elemente brauchen nicht vom gleichen Typ zu sein. Auf die einzelnen Elemente kann man über eckige Klammern [, ], über getindex oder first und last zugreifen.

julia> Adresse = ("Marie", "Kuhn", "Feldweg", 13, 183, "Siedlung")
("Marie", "Kuhn", "Feldweg", 13, 183, "Siedlung")

julia> Adresse[5]
183

julia> getindex(Adresse, 3)
"Feldweg"

julia> Adresse[5] = 333 # Nicht möglich! Das Tupel ist nicht veränderbar.

Mit eckigen Klammern [, ] erzeugt man ein Array. Wiederum brauchen die Elemete nicht vom gleichen Typ zu sein. Auf die einzelnen Elemente kann man über eckige Klammern [, ] oder über getindex und setindex zugreifen.

julia> gebäudedaten = ["Feldweg 13", 13.3, 34.9, 399.4, "Kuhn"] 
5-element Array{Any,1}:
    "Feldweg 13"
  13.3          
  34.9          
 399.4          
    "Kuhn"      

julia> gebäudedaten[3]
34.9

julia> gebäudedaten[3] = 0
0

julia> gebäudedaten
5-element Array{Any,1}:
    "Feldweg 13"
  13.3          
   0            
 399.4          
    "Kuhn"      

Über die Angabe eines Bereichs, Range, lassen sich bequem Arrays von äquidistanten Elementen erzeugen, oder for-Schleifen formulieren. Der Doppelpunkt trennt den Startwert vom Endwert, optional kann eine Schrittweite zwischen Startwert und Endwert angegeben werden.

julia> for i in 1:7
          println(i)
       end
1
2
3
4
5
6
7

julia> va = collect(1:7)
7-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7

julia> vb = [8:14;]
7-element Array{Int64,1}:
  8
  9
 10
 11
 12
 13
 14

julia> [0:0.05:0.8;]
17-element Array{Float64,1}:
 0.0 
 0.05
 0.1 
 0.15
 0.2 
 0.25
 0.3 
 0.35
 0.4 
 0.45
 0.5 
 0.55
 0.6 
 0.65
 0.7 
 0.75
 0.8 

Schleifen lassen sich im Übrigen durch mehrere Iterationsangaben schachteln

julia> for i = 1:3, j in (5, 7)
          println((i, j))
       end
(1, 5)
(1, 7)
(2, 5)
(2, 7)
(3, 5)
(3, 7)

Comprehensions und mehrdimendisonale Arrays

Die Funktionen zeros(n) und ones(n) erzeugen Arrays vom Typ Float64 oder Float32 der Länge n. Die Funktionen zeros(n) und ones(n)akzeptieren auch die Angabe eines Typs und die Angabe von Grenzen in mehreren Dimensionen, so dass mehrdimensionale Arrays erhalten werden.

julia> ones(7)
7-element Array{Float64,1}:
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0

julia> zeros(Int, 3, 4)
3×4 Array{Int64,2}:
 0  0  0  0
 0  0  0  0
 0  0  0  0

Komplexere Arrays erzeugt man durch Comprehensions. In eckigen Klammern wird angegeben, wie die Elemente des Arrays erzeugt werden sollen. Werden mehrere Interationsangaben gemacht, erhält man ein Array in mehreren Dimensionen.

julia> k = 5
julia> vx = [i <= k ? 1 : (2 * i) for i in 1:10]
10-element Array{Int64,1}:
  1
  1
  1
  1
  1
 12
 14
 16
 18
 20

julia> vx = [i <= k ? 1 : (2 * i) for i in 1:10, j in 1:2]
10×2 Array{Int64,2}:
  1   1
  1   1
  1   1
  1   1
  1   1
 12  12
 14  14
 16  16
 18  18
 20  20

julia> vx = [i <= k ? 1 : (2 * i + j) for i in 1:10, j in 1:2]
10×2 Array{Int64,2}:
  1   1
  1   1
  1   1
  1   1
  1   1
 13  14
 15  16
 17  18
 19  20
 21  22

Die Funktionen sum, mean, minimum, maximum und einige weitere akzeptieren Comprehensions als Argumente.

Mit dem Operator ... lassen sich in einem Funktionsaufruf Sequenzen auflösen. Man vergleiche:

julia> tuple(ones(3), zeros(2))
([1.0, 1.0, 1.0], [0.0, 0.0])

julia> tuple(ones(3)..., zeros(2)...)
(1.0, 1.0, 1.0, 0.0, 0.0)

Eindimensionale Arrays lassen sich zu zweidimensionalen Arrays durch Auflistung getrennt durch Strichpunkte vertikal (Dimension 1) und getrennt durch Leerzeichen horizontal (Dimension 2) zusammenfügen. Die entsprechenden Funktione heißen vcat und hcat. Allgemein erhält man höherdimensionale Arrays über die Funktion reshape, deren erstes Argument eine Sequenz ist. Die weiteren Argumente sind die Grenzen in den verschiedenen Dimensionen, wobei eine der Grenzen unbestimmt sein darf. Sie wird mit einem Doppelpunkt : gekennzeichnet. An den Ergebnissen wird auch deutlich, in welcher Reihenfolge die Elemente in höherdimensionalen Arrays in Julia durchlaufen werden.

julia> s1 = collect(21:25)
5-element Array{Int64,1}:
 21
 22
 23
 24
 25

julia> [s1; s1]
10-element Array{Int64,1}:
 21
 22
 23
 24
 25
 21
 22
 23
 24
 25

julia> [s1 s1]
5×2 Array{Int64,2}:
 21  21
 22  22
 23  23
 24  24
 25  25

julia> reshape(collect(21:40), 2, :)
2×10 Array{Int64,2}:
 21  23  25  27  29  31  33  35  37  39
 22  24  26  28  30  32  34  36  38  40

julia> reshape(collect(21:40), 2, 5, :)
2×5×2 Array{Int64,3}:
[:, :, 1] =
 21  23  25  27  29
 22  24  26  28  30

[:, :, 2] =
 31  33  35  37  39
 32  34  36  38  40

julia> reshape(collect(21:40), 2, :, 5)
2×2×5 Array{Int64,3}:
[:, :, 1] =
 21  23
 22  24

[:, :, 2] =
 25  27
 26  28

[:, :, 3] =
 29  31
 30  32

[:, :, 4] =
 33  35
 34  36

[:, :, 5] =
 37  39
 38  40

Arrays und Tuple können beliebige Elemente enthalten, also auch Arrays und Tuple.

julia> städte = [["München", "Bamberg", "Nürnberg"], 
                 ["Stuttgart", "Freiburg", "Karlsruhe", "Konstanz"], 
                 ["Frankfurt", "Darmstadt", "Offenbach"], 
                 ["Mainz", "Trier"]]
4-element Array{Array{String,1},1}:
 String["München", "Bamberg", "Nürnberg"]                
 String["Stuttgart", "Freiburg", "Karlsruhe", "Konstanz"]
 String["Frankfurt", "Darmstadt", "Offenbach"]           
 String["Mainz", "Trier"]

Indexierung

Auf Elemente mehrdimensionaler Arrays wird zugegriffen durch die Angabe des Index in jeder der Dimensionen. Es kann aber auch nur ein einzelner linearer Index angegeben werden. Es ist auch möglich Bereiche anzugeben (slicing), zum Beispiel als Liste, durch jeweilige Grenzen mit Doppelpunkt : getrennt oder nur durch einen Doppelpunkt :. Die Bereiche brauchen nicht zusammenhängend zu sein.

julia> q1 = reshape(collect(21:40), 4, :)
4×5 Array{Int64,2}:
 21  25  29  33  37
 22  26  30  34  38
 23  27  31  35  39
 24  28  32  36  40

julia> q1[2, 4]
34

julia> q1[14]
34

julia> q1[2:3,3:4]
2×2 Array{Int64,2}:
 30  34
 31  35

julia> q1[[1,3],[2, 4]]
2×2 Array{Int64,2}:
 25  33
 27  35
 
 julia> q1[[1,3],:]
2×5 Array{Int64,2}:
 21  25  29  33  37
 23  27  31  35  39

Außerdem sind Bit-Arrays, bzw. Arrays mit Wahrheitswerten als Elemente einer Indizierung erlaubt.

julia> s2 = collect(20:20:100)
5-element Array{Int64,1}:
  20
  40
  60
  80
 100

julia> ix = [true, true, false, false, true];

julia> s2[ix]
3-element Array{Int64,1}:
  20
  40
 100

Dictionaries

Dictionaries vom Typ Dict sind assoziative Datensammlungen. Sie enthalten Paare aus Schlüssel und Wert. Man erzeugt ein solches Verzeichnis aus einer Liste von Paaren oder aus einem Array von 2-elementigen Tupeln. Der Ausdruck a => b liefert ein Paar aus a und b. Im ersten Beispiel werden Elemente vom Typ Symbol als Schlüssel verwendet.

	julia> övnetz = Dict(:l1 => "Oberstadt", :l2 => "Unterstadt")
	Dict{Symbol,String} with 2 entries:
	  :l2 => "Unterstadt"
	  :l1 => "Oberstadt"

	julia> jahrgang = Dict("5a" => 23, "5b" => 27, "5c" => 19)
	Dict{String,Int64} with 3 entries:
	  "5c" => 19
	  "5b" => 27
	  "5a" => 23

Falls Schlüssel und Werte als Arrays vorliegen, kann man sie über die Funktion zip oder über Broadcasting leicht zu einem Verzeichnis zusammenführen.

	julia> lp = ["a", "b", "c"]
	3-element Array{String,1}:
	"a"
	"b"
	"c"

	julia> coord = [(0.4, 0.5), (0.3, 0.8), (0.9, 0.7)]
	3-element Array{Tuple{Float64,Float64},1}:
	(0.4, 0.5)
	(0.3, 0.8)
	(0.9, 0.7)

	julia> dpc = Dict(zip(lp, coord))
	Dict{String,Tuple{Float64,Float64}} with 3 entries:
	"c" => (0.9, 0.7)
	"b" => (0.3, 0.8)
	"a" => (0.4, 0.5)
	
	# oder
	
	julia> dpc = Dict(lp .=> coord)
	Dict{String,Tuple{Float64,Float64}} with 3 entries:
	  "c" => (0.9, 0.7)
	  "b" => (0.3, 0.8)
	  "a" => (0.4, 0.5)

Über den Indexoperator [] lässt sich ein Verzeichnis sowohl abfragen, als auch befüllen. Weitere Funktionen, die mit Verzeichnissen arbeiten, sind get, get!, haskey, keys, values, delete!.

	julia> jahrgang["5b"]
	27

	julia> jahrgang["5f"] = 17
	17

	julia> jahrgang
	Dict{String,Int64} with 4 entries:
	  "5f" => 17
	  "5c" => 19
	  "5b" => 27
	  "5a" => 23

Funktionen

Eine einfache Funktionsdefinition ist die Zuweisung einer Anweisung oder eines Anweisungsblocks zu einer Variablen. Bei komplexeren Definitionen leitet man die Definition mit dem Schlüsselwort function ein.

dreisatz(a, b, g) = (r = a/b; return r, r * g)

function fib(n)
    afib = [0; 1]
    for i = 2:n
        push!(afib, afib[i-1] + afib[i])
    end
    afib
end

Funktionen können mit benannten Argumenten und optionalen Argumenten defniert werden.

Julia unterstützt multiple dispatch. D.h. einem Funktionennamen sind in der Regel mehrere Methoden zugeordnet. Julia unterscheidet die Methoden anhand ihrer Argumente, mit denen sie aufgerufen werden. Das erweist sich als ein klares und sehr vielseitiges Konzept. Im Gegensatz zum objektorientierten Ansatz brauchen Methoden keinem Objekt zugeordnet zu sein.

Ein gutes Beispiel ist die Funktion rand, mit der Zufallszahlen erzeugt werden. Sie umfasst unter anderem folgende Methoden:

Argument Ergebnis
kein Argument Zahl aus dem Intervall [0, 1]
eine natürliche Zahl n Array der Länge n aus Zahlen aus [0, 1]
k natürliche Zahlen k-dimensiones Array
Typ Eine Zahl des Typs; für Fließkommatypen aus [0, 1]
Bereich, Tupel oder Array Eine Zahl aus dem Bereich, Tupel oder Array
Bereich, Tupel oder Array mit einer natürlichen Zahl n Array der Länge naus Elementen des Bereichs, des Tupels oder des Arrays
julia> rand()
0.9855776799095464

julia> rand(7)
7-element Array{Float64,1}:
 0.262607
 0.346188
 0.737495
 0.501209
 0.433388
 0.87786 
 0.455923

julia> rand(3, 4)
3×4 Array{Float64,2}:
 0.0995068  0.357325  0.470847  0.810889
 0.806012   0.282845  0.01928   0.974902
 0.43434    0.646671  0.282743  0.524794

julia> rand(UInt)
0x5322825f42aa180c

julia> rand([5, 10, 50, 100])
50

julia> rand(4:10, 3)
3-element Array{Int64,1}:
 6
 4
 5

Um den Mechanismus des Überladens zu nutzen, spezifiziert man die Typen der Argumente. Der Typ wird getrennt durch einen zweifachen Doppelpunkt :: angegeben.

julia> function wurzel(n::Int)
           vw = sqrt(n)
           isinteger(vw) || error("$n hat keine ganzzahlige Wurzel")
           vw
        end
	   
julia> wurzel(n) = sqrt(n)

julia> wurzel(9)
3

julia> wurzel(10)
ERROR: 10 hat keine ganzzahlige Wurzel
Stacktrace:
 [1] wurzel(::Int64) at ./REPL[15]:6

julia> wurzel(10.0)
3.1622776601683795

Julia verwendet immer die Funktion, deren Argumentliste für die aktuellen Argumente möglichst spezifisch ist.

Julia erlaubt es, Funktionen innerhalb anderer Funktionen zu definieren. Funktionen können auch selbst als Argumente übergeben werden. Außerdem kennt Julia anonyme Funktionen. Vergleiche lambda-Ausdrücke in Lisp.

julia> fless(x, f1, f2) = (f1(x) < f2(x))
fless (generic function with 1 method)

julia> fless(0.0, sin, cos)
true

julia> fless(pi, sin, cos)
false

julia> fless(3, x -> 2x, x -> x^2)
true

julia> fless(0.3, x -> 2x, x -> x^2)
false

Strukturierte Datentypen

Daten können in eigenen Datentypen zusammengefasst werden. Eine entsprechende Definition wird mit struct eingeleitet. Sollen die Werte eines Datentyps veränderbar sein, muss der Datentyp als mutable struct definiert werden.

julia> using Dates
julia> mutable struct Ortsteil
           name::String
           fläche::Float64
           neinwohner::Int
           gründung::Date
       end

Julia erzeugt standardmäßig einen Konstruktor mit einem Argument für jedes Element. Es können weitere Konstruktoren definiert werden, beispielsweise, um Objekte mit Standardwerten zu erzeugen.

julia> Ortsteil(name, fläche; n = 0, datum = today()) = Ortsteil(name, fläche, n, datum)
Ortsteil

julia> oto = Ortsteil("Neusiedlung", 30.0)
Ortsteil("Neusiedlung", 30.0, 0, 2018-08-15)

fieldnames liefert für einen zusammengesetzten Datentyp die Elementnamen des Datentyps. Über den Punkt-Operator . oder über die Funktion getfield greift man auf die Elemente eines Objektes zu.

julia> fieldnames(Ortsteil)
(:name, :fläche, :neinwohner, :gründung)

julia> oto = Ortsteil("Neusiedlung", 30.0)
Ortsteil("Neusiedlung", 30.0, 0, 2018-02-28)

julia> oto.name
"Neusiedlung"

julia> getfield(oto, :neinwohner)
0

Funktoren

Datentypen können auch zu ausführbaren Objekten, Funktoren, gemacht werden. So ist es möglich, Parameter im Funktor abzulegen, anstatt sie jedesmal in einem Funktionsaufruf anzugeben.

julia> struct Update!
          vmax::Int
       end

julia> function (u::Update!)(pos)
          pos += u.vmax
       end

julia> fobj = Update!(3)
Update!(3)

julia> fobj(7)
10
julia> versioninfo()
Julia Version 1.0.0
Commit 5d4eaca0c9 (2018-08-08 20:58 UTC)

Kategorien:

Aktualisiert: