4  Grundlagen der Syntax

4.1 Namen von Variablen, Funktionen, Typen etc.

  • Namen können Buchstaben, Ziffern, den Unterstrich _ und das Ausrufezeichen ! enthalten.
  • Das erste Zeichen muss ein Buchstabe oder ein Unterstrich sein.
  • Groß- und Kleinbuchstaben werden unterschieden: Nmax und NMAX sind verschiedene Variablen.
  • Als Zeichensatz wird Unicode verwendet. Damit stehen über 150 Schriften und zahlreiche Symbole zur Verfügung.
  • Es gibt eine kurze Liste reservierter Schlüsselwörter: if, then, function, true, false,...
Beispiel

zulässig: i, x, Ω, x2, DieUnbekannteZahl, neuer_Wert, 🎷, Zähler, лічильник, einself!!!!,...

unzulässig: Uwe's_Funktion, 3achsen, A#B, $this_is_not_Perl, true,...


Anmerkung

Neben den reserved keywords der Kernsprache sind zahlreiche weitere Funktionen und Objekte vordefiniert, wie z.B. die mathematischen Funktionen sqrt(), log(), sin(). Diese Definitionen finden sich in dem Modul Base, welches Julia beim Start automatisch lädt. Namen aus Base können umdefiniert werden, solange sie noch nicht verwendet wurden:

log = 3
1 + log
4

Jetzt ist natürlich der Logarithmus kaputt:

x = log(10)
LoadError: MethodError: objects of type Int64 are not callable
Maybe you forgot to use an operator such as *, ^, %, / etc. ?

Stacktrace:
 [1] top-level scope
   @ In[3]:1

(siehe auch https://stackoverflow.com/questions/65902105/how-to-reset-any-function-in-julia-to-its-original-state)

4.2 Anweisungen

  • Im Normalfall enthält eine Zeile eine Anweisung.
  • Wenn eine Anweisung am Zeilenende als unvollständig erkennbar ist durch
    • offene Klammern
    • Operationszeichen,
    dann wird die nächste Zeile als Fortsetzung aufgefasst.
  • Mehrere Anweisungen pro Zeile können durch Semikolon getrennt werden.
  • Im interaktiven Betrieb (REPL oder Notebook) unterdrückt ein Semikolon nach der letzten Anweisung die Ausgabe des Ergebnisses dieser Anweisung.
Beispiel

Im interaktiven Betrieb wird der Wert der letzten Anweisung auch ohne explizites print() ausgegeben:

println("Hallo 🌍!")
x = sum([i^2 for  i=1:10])
Hallo 🌍!
385

Das Semikolon unterdrückt das:

println("Hallo 🌍!")
x = sum([i^2 for  i=1:10]);
Hallo 🌍!

Warnung

Bei mehrzeiligen Anweisungen muss die fortzusetzende Zeile mit einer offenen Operation oder Klammer enden:

x = sin(π/2) +
   3 * cos(0)
4.0

Also geht das Folgende schief, aber leider ohne eine Fehlermeldung!

x = sin(π/2)
  + 3 * cos(0)
println(x)
1.0

Hier wird das + in der zweiten Zeile als Präfix-Operator (Vorzeichen) interpretiert. Damit sind 1. und 2. Zeile jeweils für sich vollständige, korrekte Ausdrücke (auch wenn die 2. Zeile natürlich völlig nutzlos ist) und werden auch so abgearbeitet.

Moral: Wenn man längere Ausdrücke auf mehrere Zeilen aufteilen will, sollte man immer eine Klammer aufmachen. Dann ist egal, wo der Zeilenumbruch ist:

x = ( sin(π/2) 
   + 3 * cos(0) )
println(x)
4.0

4.3 Kommentare

Julia kennt 2 Arten von Kommentaren im Programmtext:

# Einzeilige Kommentare beginnen mit einem Doppelkreuz.

x = 2    # alles vom '#' bis zum Zeilenende ist ein Kommentar und wird ignoriert. x = 3
2
#= 
   Ein- und mehrzeilige Kommentare können zwischen #= ...  =# eingeschlossen werden.
   Dabei sind verschachtelte Kommentare möglich. 
   #= 
      d.h., anders als in C/C++/Java endet der Kommentar nicht mit dem ersten 
      Kommentar-Endezeichen, sondern die #=...=# - Paare wirken wie Klammern.    
   =# 
   Der automatische 'syntax highlighter' weiss das leider noch nicht, wie die wechselnde 
   Graufärbung dieses Kommentars zeigt.     
=#


x #= das ist ein seltener Variablenname! =#  = 3 
3

4.4 Datentypen Teil I

  • Julia ist eine stark typisierte Sprache. Alle Objekte haben einen Typ. Funktionen/Operationen erwarten Argumente mit dem richtigen Typ.
  • Julia ist eine dynamisch typisierte Sprache. Variablen haben keinen Typ. Sie sind Namen, die durch Zuweisung x = ... an Objekte gebunden werden können.
  • Wenn man vom „Typ einer Variablen“ spricht, meint man den Typ des Objektes, das der Variablen gerade zugewiesen ist.
  • Funktionen/Operatoren können verschiedene methods für verschiedene Argumenttypen implementieren.
  • Abhängig von den konkreten Argumenttypen wird dann bei Verwendung einer Funktion entschieden, welche Methode benutzt wird (dynamic dispatch).

Einfache Basistypen sind z.B.:

Int64, Float64, String, Char, Bool
x = 2
x, typeof(x), sizeof(x) 
(2, Int64, 8)
x = 0.2
x, typeof(x), sizeof(x) 
(0.2, Float64, 8)
x = "Hallo!"
x, typeof(x), sizeof(x) 
("Hallo!", String, 6)
x = 'Ω'
x, typeof(x), sizeof(x) 
('Ω', Char, 4)
x = 3 > π
x, typeof(x), sizeof(x) 
(false, Bool, 1)
  • sizeof() liefert die Größe eines Objekts oder Typs in Bytes (1 Byte = 8 Bit)
  • 64bit Ganzzahlen und 64bit Gleitkommazahlen entsprechen dem Befehlssatz moderner Prozessoren und sind daher die numerischen Standardtypen.
  • Zeichen/chars 'A' und Zeichenketten/strings "A" der Länge 1 sind verschiedene Objekte.

4.5 Ablaufsteuerung

4.5.1 if-Blöcke

  • Ein if-Block kann beliebig viele elseif-Zweige und als letztes maximal einen else-Zweig enthalten.
  • Der Block hat einen Wert, den Wert der letzten ausgeführten Anweisung.
x = 33
y = 44
z = 34

if x < y && z != x                     # elseif- und else-Blöcke sind optional
    println("yes")
    x += 10
elseif x < z                           # beliebig viele elseif-Blöcke
    println(" x is smaller than z")
elseif x == z+1                           
    println(" x is successor of z")
else                                   # maximal ein else-Block 
    println("Alles falsch")
end                                    # Wert des gesamten Blocks ist der Wert der 
                                       # letzten ausgeführten Auswertung
yes
43

Kurze Blöcke kann man in eine Zeile schreiben:

if x > 10 println("x is larger than 10") end 
x is larger than 10

Der Wert eines if-Blocks kann natürlich zugewiesen werden:

y = 33
z = if y > 10 
      println("y is larger than 10") 
      y += 1 
    end
z    
y is larger than 10
34

4.5.2 Auswahloperator (ternary operator) test ? exp1 : exp2

x = 20
y = 15
z = x < y ? x+1 : y+1
16

ist äquivalent zu

z = if x < y
        x+1
    else
        y+1
    end
16

4.6 Vergleiche, Tests, Logische Operationen

4.6.1 Arithmetische Vergleiche

  • ==
  • !=,
  • >
  • >=,
  • <
  • <=,

Wie üblich, ist der Test auf Gleichheit == vom Zuweisungsoperator = zu unterscheiden. Vergleichen lässt sich so gut wie alles:

"Aachen" < "Leipzig", 10  10.01, [3,4,5] < [3,6,2]
(true, true, true)

Nun ja, fast alles:

3 < "vier"
LoadError: MethodError: no method matching isless(::Int64, ::String)

Closest candidates are:
  isless(::Missing, ::Any)
   @ Base missing.jl:87
  isless(::Any, ::Missing)
   @ Base missing.jl:88
  isless(::Real, ::AbstractFloat)
   @ Base operators.jl:178
  ...


Stacktrace:
 [1] <(x::Int64, y::String)
   @ Base ./operators.jl:352
 [2] top-level scope
   @ In[22]:1

Die Fehlermeldung zeigt ein paar Grundprinzipien von Julia:

  • Operatoren sind auch nur Funktionen: x < y wird zum Funktionsaufruf isless(x, y).
  • Funktionen (und damit Operatoren) können verschiedene methods für verschiedene Argumenttypen implementieren.
  • Abhängig von den konkreten Argumenttypen wird beim Aufruf der Funktion entschieden, welche Methode benutzt wird (dynamic dispatch).

Man kann sich alle Methoden zu einer Funktion anzeigen lassen. Das gibt einen Einblick in das komplexe Typssystem von Julia:

methods(<)
# 76 methods for generic function < from ꍟ⦃90mBaseꍟ⦃39m:

Zuletzt noch: Vergleiche können gekettet werden.

10 < x  100  # das ist äquivalent zu
              #   10 < x && x ≤ 100
true

4.6.2 Tests

Einge Funktionen vom Typ f(c::Char) -> Bool

isnumeric('a'), isnumeric('7'), isletter('a')
(false, true, true)

und vom Typ f(s1::String, s2::String) -> Bool

contains("Lampenschirm", "pensch"), startswith("Lampenschirm", "Lamb"), endswith("Lampenschirm", "rm")
(true, false, true)
  • Die Funktion in(item, collection) -> Bool testet, ob item in collection ist.
  • Sie hat auch das Alias ∈(item, collection) und
  • sowohl in als auch können auch als Infix-Operatoren geschrieben werden.
x = 3
x in [1, 2, 3, 4, 5]
true
x  [1, 2, 33, 4, 5]
false

4.6.3 Logische Operationen: &&, ||, !

3 < 4 && !(2 > 8) && !contains("aaa", "b")
true

Bedingte Auswertung (short circuit evaluation)

  • in a && b wird b nur ausgewertet, wenn a == true
  • in a || b wird b nur ausgewertet, wenn a == false
  1. Damit kann if test statement end auch als test && statement geschrieben werden.

  2. Damit kann if !test statement end als test || statement geschrieben werden.

Als Beispiel1 hier eine Implementierung der Fakultätsfunktion (factorial):

function fact(n::Int)
    n >= 0 || error("n must be non-negative")
    n == 0 && return 1
    n * fact(n-1)
end

fact(5)
120

Natürlich kann man alle diese Tests auch Variablen vom Typ Bool zuordnen und diese Variablen können als Tests in if- und while-Blöcken verwendet werden:

x = 3 < 4
y = 5  [1, 2, 5, 7]
z = x && y
if z                        # äquivalent zu:  if 3 < 4  &&  5 in [1,2,5,7]
    println("Stimmt alles!")
end
Stimmt alles!
  • In Julia müssen alle Tests in einem logischen Ausdruck vom Typ Bool sein.
  • Es gibt keine implizite Konvertierung à la “0 is false and 1 (or anything != 0) is true”
  • Wenn x ein numerischer Typ ist, dann muss daher das C-Idiom if(x) als if x != 0 geschrieben werden.
  • Es gibt eine Ausnahme zur Unterstützung der short circuit evaluation:
    • bei den Konstrukten a && b && c... bzw a || b || c... muss der letzte Teilausdruck nicht vom Typ Bool sein, wenn diese Konstrukte nicht als Tests in if oder while verwendet werden:
z = 3 < 4 && 10 < 5 &&  sqrt(3^3)
z, typeof(z)
(false, Bool)
z = 3 < 4 && 10 < 50 &&  sqrt(3^3)
z, typeof(z)
(5.196152422706632, Float64)

4.7 Schleifen (loops)

4.7.1 Die while (“solange”)-Schleife

Syntax:

while *condition*
   *loop body*
end

Eine Reihe von Anweisungen (der Schleifenkörper) wird immer wieder abgearbeitet, solange eine Bedingung erfüllt ist.

i = 1          # typischerweise braucht der Test der 
               # while-Schleife eine Vorbereitung ...
while i < 10   
   println(i)
   i += 2      # ... und ein update
end
1
3
5
7
9

Der Körper einer while- und for-Schleife kann die Anweisungen break und continue enthalten. break stoppt die Schleife, continue überspringt den Rest des Schleifenkörpers und beginnt sofort mit dem nächsten Schleifendurchlauf.

i = 0

while i<10
   i += 1

   if i == 3
      continue     # beginne sofort nächsten Durchlauf,
   end             #  überspringe Rest des Schleifenkörpers

   println("i = $i")

   if i  5
      break        # breche Schleife ab
   end
end

println("Fertig!")
i = 1
i = 2
i = 4
i = 5
Fertig!

Mit break kann man auch Endlosschleifen verlassen:

i = 1

while true
    println(2^i)
    i += 1
    if i > 8 break end
end
2
4
8
16
32
64
128
256

4.7.2 for-Schleifen

Syntax:

for *var* in *iterable container*
   *loop body*
end

Der Schleifenkörper wird für alle Items aus einem Container durchlaufen.

Statt in kann immer auch \(\in\) verwendet werden. Im Kopf einer for-Schleife kann auch = verwendet werden.

for i  ["Mutter", "Vater", "Tochter"]
    println(i)
end
Mutter
Vater
Tochter

Oft benötigt man einen numerischen Schleifenzähler. Dafür gibt es das range-Konstrukt. Die einfachsten Formen sind Start:Ende und Start:Schrittweite:Ende.

endwert = 5

for i  1:endwert
    println(i^2)
end
1
4
9
16
25
for i = 1:5.5  print(" $i") end
 1.0 2.0 3.0 4.0 5.0
for i = 1:2:14 print("  $i") end
  1  3  5  7  9  11  13
for k = 14 : -2.5 : 1 print("   $k") end
   14.0   11.5   9.0   6.5   4.0   1.5

Geschachtelte Schleifen (nested loops)

Ein break beendet die innerste Schleife.

for i = 1:3
    for j = 1:3
        println( (i,j) )
        if j == 2
            break
        end
    end
end
(1, 1)
(1, 2)
(2, 1)
(2, 2)
(3, 1)
(3, 2)

Man kann nested loops auch in einer for-Anweisung zusammenfassen. Dann beendet ein break die Gesamtschleife.

for i = 1:3, j=1:3     # im Prinzip dasselbe wie oben, aber:
    println( (i,j) )
    if j == 2
        break          # break bricht hier die Gesamtschleife ab
    end
end
(1, 1)
(1, 2)
Wichtig: Die Semantik ist völlig anders, als bei C-artigen for-Schleifen!

Bei jedem Schleifendurchlauf wird die Laufvariable neu mit dem nächsten Element aus dem Container initialisiert.

for i = 1:5
    print(i," ... ")
    i += 2
    println(i)
end
1 ... 3
2 ... 4
3 ... 5
4 ... 6
5 ... 7

Die C-Semantik von for(i=1; i<5; i++) entspricht der while-Schleife:

i = 1
while i<5
   *loop body*   # hier kann auch wirksam an i rumgepfuscht werden
   i += 1
end

4.8 Unicode

Julia verwendet Unicode als Zeichensatz. Damit können für Variablen, Funktionen etc auch Bezeichner aus nicht-lateinischen Schriften (zB Kyrillisch, Koreanisch, Sanskrit, Runen, Emoji,…) verwendet werden. Die Frage, wie man solche Zeichen in seinem Editor eingeben kann und ob der verwendete Bildschirm-Font sie darstellen kann, ist nicht Julias Problem.

  • Einige Unicode-Zeichen, z.B. ≤, ≠, ≥, π, ∈, √, können anstelle von <=, !=, >=, pi, in, sqrt verwendet werden.

  • über 3000 Unicode-Zeichen können in Julia in einer LaTeX-ähnlichen Weise mit der Tab-Vervollständigung eingegeben werden.

4.9 Eigenheiten und Stolperfallen der Syntax

  • Man kann den Multiplikationsoperator * nach einer numerischen Konstanten weglassen, wenn eine Variable, Funktion oder öffnende Klammer folgt.

     z = 3.4x + 2(x+y) + xy

ist daher korrektes Julia. Beachte allerdings, dass der Term xy als eine Variable mit dem Namen xy interpretiert wird und nicht als Produkt von x und y!

  • Diese Regel hat ein paar Tücken:

Das funktioniert wie erwartet:

e = 7
3e 
21

Hier wird die Eingabe als Gleitkommazahl interpretiert – und 3E+2 oder 3f+2 (Float32) ebenso.

3e+2 
300.0

Ein Leerzeichen schafft Eindeutigkeit:

3e + 2 
23

Das funktioniert:

x = 4
3x + 3 
15

…und das nicht. 0x, 0o, 0b wird als Anfang einer Hexadezimal-, Oktal- bzw. Binärkonstanten interpretiert.

3y + 0x  
LoadError: ParseError:
# Error @ ]8;;file:///home/hellmund/Julia/23/Book/chapters/In[49]#1:6\In[49]:1:6]8;;\
3y + 0x  
#    └┘ ── invalid numeric constant

Stacktrace:
 [1] top-level scope
   @ In[49]:1
  • Es gibt noch ein paar andere Fälle, bei denen die sehr kulante Syntax zu Überraschungen führt.
Wichtig  = 21
Wichtig! = 42       # Bezeichner können auch ein ! enthalten
(Wichtig, Wichtig!)
(21, 42)
Wichtig!=88
true

Julia interpretiert das als Vergleich Wichtig != 88.

Leerzeichen helfen:

Wichtig! = 88
Wichtig!
88
  • Operatoren der Form .*, .+,… haben in Julia eine spezielle Bedeutung (broadcasting, d.h., vektorisierte Operationen).
1.+2.
LoadError: ParseError:
# Error @ ]8;;file:///home/hellmund/Julia/23/Book/chapters/In[53]#1:1\In[53]:1:1]8;;\
1.+2.
└┘ ── ambiguous `.` syntax; add whitespace to clarify (eg `1.+2` might be `1.0+2` or `1 .+ 2`)

Stacktrace:
 [1] top-level scope
   @ In[53]:1

Wieder gilt: Leerzeichen schaffen Klarheit!

1. + 2.
3.0

  1. aus der Julia-Dokumentation↩︎