10  Funktionen und Operatoren

Funktionen verarbeiten ihre Argumente zu einem Ergebnis, das sie beim Aufruf zurückliefern.

10.1 Formen

Funktionen können in verschiedenen Formen definiert werden:

  1. Als function ... end-Block
function hyp(x,y)
    sqrt(x^2+y^2)
end
hyp (generic function with 1 method)
  1. Als “Einzeiler”
hyp(x, y) = sqrt(x^2 + y^2)
hyp (generic function with 1 method)
  1. Als anonyme Funktionen
(x, y) -> sqrt(x^2 + y^2)
#1 (generic function with 1 method)

10.1.1 Block-Form und return

  • Mit return wird die Abarbeitung der Funktion beendet und zum aufrufenden Kontext zurückgekehrt.
  • Ohne return wird der Wert des letzten Ausdrucks als Funktionswert zurückgegeben.

Die beiden Definitionen

function xsinrecipx(x)
    if x == 0 
        return 0.0
    end 
    return x * sin(1/x)
end

und ohne das zweite explizite return in der letzten Zeile:

function xsinrecipx(x)
    if x == 0 
        return 0.0
    end 
    x * sin(1/x)
end

sind also äquivalent.

  • Eine Funktion, die “nichts” zurückgibt (void functions in der C-Welt), gibt den Wert nothing vom Typ Nothing zurück. (So wie ein Objekt vom Typ Bool die beiden Werte true und false haben kann, so kann ein Objekt vom Typ Nothing nur einen einzigen Wert, eben nothing, annehmen.)
  • Eine leere return-Anweisung ist äquivalent zu return nothing.
function fn(x)
    println(x)
    return 
end

a = fn(2)
2
a
@show a typeof(a);
a = nothing
typeof(a) = Nothing

10.1.2 Einzeiler-Form

Die Einzeilerform ist eine ganz normale Zuweisung, bei der links eine Funktion steht.

hyp(x, y) = sqrt(x^2 + y^2)

Julia kennt zwei Möglichkeiten, mehrere Anweisungen zu einem Block zusammenzufassen, der an Stelle einer Einzelanweisung stehen kann:

  • begin ... end-Block
  • Eingeklammerte durch Semikolon getrennte Anweisungen.

In beiden Fällen ist der Wert des Blockes der Wert der letzten Anweisung.

Damit funktioniert auch

hyp(x, y) = (z = x^2; z += y^2; sqrt(z))

und

hyp(x, y) = begin 
                z = x^2
                z += y^2 
                sqrt(z) 
            end

10.1.3 Anonyme Funktionen

Anonyme FUnktionen kann man der Anonymität entreisen, indem man ihnen einen Namen zuweist.

hyp = (x,y) -> sqrt(x^2 + y^2)

Ihre eigentliche Anwendung ist aber im Aufruf einer (higher order) Funktion, die eine Funktion als Argument erwartet.

Typische Anwendungen sind map(f, collection), welches eine Funktion auf alle Elemente einer Kollektion anwendet. In Julia funktioniert auch map(f(x,y), collection1, collection2):

map( (x,y) -> sqrt(x^2 + y^2), [3, 5, 8], [4, 12, 15])
3-element Vector{Float64}:
  5.0
 13.0
 17.0
map( x->3x^3, 1:8 )
8-element Vector{Int64}:
    3
   24
   81
  192
  375
  648
 1029
 1536

Ein weiteres Beispiel ist filter(test, collection), wobei ein Test eine Funktion ist, die ein Bool zurückgibt.

filter(x -> ( x%3 == 0 && x%5 == 0), 1:100  )
6-element Vector{Int64}:
 15
 30
 45
 60
 75
 90

10.2 Argumentübergabe

  • Beim Funktionsaufruf werden von den als Funktionsargumente zu übergebenden Objekten keine Kopien gemacht. Die Variablen in der Funktion verweisen auf die Originalobjekte. Julia nennt dieses Konzept pass_by_sharing.
  • Funktionen können also ihre Argumente wirksam modifizieren, falls es sich um mutable Objekte, wie z.B. Vector, Array handelt.
  • Es ist eine Konvention in Julia, dass die Namen von solchen argumentmodifizierenden Funktionen mit einem Ausrufungszeichen enden. Weiterhin steht dann üblicherweise das Argument, das modifiziert wird, an erster Stelle und es ist auch der Rückgabewert der Funktion.
V = [1, 2, 3]

W = fill!(V, 17)
                        # '===' ist Test auf Identität
@show  V  W  V===W;     # V und W benennen dasselbe Objekt
V = [17, 17, 17]
W = [17, 17, 17]
V === W = true
function fill_first!(V, x)
        V[1] = x
        return V    
end                     

U = fill_first!(V, 42)

@show V   U   V===U;
V = [42, 17, 17]
U = [42, 17, 17]
V === U = true

10.3 Varianten von Funktionsargumenten

  • Es gibt Positionsargumente (1. Argument, 2. Argument, ….) und keyword-Argumente, die beim Aufruf durch ihren Namen angesprochen werden müssen.
  • Sowohl Positions- als auch keyword-Argumente können default-Werte haben. Beim Aufruf können diese Argumente weggelassen werden.
  • Die Reihenfolge der Deklaration muss sein:
    1. Positionsargumente ohne Defaultwert,
    2. Positionsargumente mit Defaultwert,
    3. — Semikolon —,
    4. kommagetrennte Liste der Keywordargumente (mit oder ohne Defaultwert)
  • Beim Aufruf können keyword-Argumente an beliebigen Stellen in beliebiger Reihenfolge stehen. Man kann sie wieder durch ein Semikolon von den Positionsargumenten abtrennen, muss aber nicht.
fa(x, y=42; a) = println("x=$x, y=$y, a=$a")

fa(6, a=4, 7)
fa(6, 7; a=4)
fa(a=-2, 6)
x=6, y=7, a=4
x=6, y=7, a=4
x=6, y=42, a=-2

Eine Funktion nur mit keyword-Argumenten wird so deklariert:

fkw(; x=10, y) = println("x=$x, y=$y")

fkw(y=2)
x=10, y=2

10.4 Funktionen sind ganz normale Objekte

  • Sie können zugewiesen werden
f2 = sqrt
f2(2)
1.4142135623730951
  • Sie können als Argumente an Funktionen übergeben werden.
# sehr naive numerische Integration

function Riemann_integrate(f, a, b; NInter=1000)
    delta = (b-a)/NInter
    s = 0
    for i in 0:NInter-1
        s += delta * f(a + delta/2 + i * delta)
    end
    return s
end


Riemann_integrate(sin, 0, π)
2.0000008224672694
  • Sie können von Funktionen erzeugt und als Ergebnis returnt werden.
function generate_add_func(x)
    function addx(y)
        return x+y
    end
    return addx
end
generate_add_func (generic function with 1 method)
h =  generate_add_func(4)
(::Main.Notebook.var"#addx#12"{Int64}) (generic function with 1 method)
h(1)
5
h(2), h(10)
(6, 14)

Die obige Funktion generate_add_func() lässt sich auch kürzer definieren. Der innere Funktionsname addx() ist sowieso lokal und außerhalb nicht verfügbar. Also kann man eine anonyme Funktion verwenden.

generate_add_func(x) = y -> x + y
generate_add_func (generic function with 1 method)

10.5 Zusammensetzung von Funktionen: die Operatoren \(\circ\) und |>

  • Die Zusammensetzung (composition) von Funktionen kann auch mit dem Operator \(\circ\) (\circ + Tab) geschrieben werden

\[(f\circ g)(x) = f(g(x))\]

(sqrt  + )(9, 16)
5.0
f = cos  sin  (x->2x)
f(.2)
0.9251300429004277
@show map(uppercase  first, ["ein", "paar", "grüne", "Blätter"]);
map(uppercase ∘ first, ["ein", "paar", "grüne", "Blätter"]) = ['E', 'P', 'G', 'B']
  • Es gibt auch einen Operator, mit dem Funktionen “von rechts” wirken und zusammengesetzt werden können (piping)
25 |> sqrt
5.0
1:10 |> sum |> sqrt
7.416198487095663
  • Natürlich kann man auch diese Operatoren ‘broadcasten’ (s. Kapitel 12.7). Hier wirkt ein Vektor von Funktionen elementweise auf einen Vektor von Argumenten:
["a", "list", "of", "strings"] .|> [length, uppercase, reverse, titlecase]
4-element Vector{Any}:
 1
  "LIST"
  "fo"
  "Strings"

10.6 Die do-Notation

Eine syntaktische Besonderheit zur Definition anonymer Funktionen als Argumente anderer Funktionen ist die do-Notation.

Sei higherfunc(f,a,...) eine Funktion, deren 1. Argument eine Funktion ist.

Dann kann man higherfunc() auch ohne erstes Argument aufrufen und statt dessen die Funktion in einem unmittelbar folgenden do-Block definieren:

higherfunc(a, b) do x, y
   Körper von f(x,y)
end

Am Beispiel von Riemann_integrate() sieht das so aus:

# das ist dasselbe wie Riemann_integrate(x->x^2, 0, 2)

Riemann_integrate(0, 2) do x x^2 end
2.6666659999999993

Der Sinn besteht natürlich in der Anwendung mit komplexeren Funktionen, wie diesem aus zwei Teilstücken zusammengesetzten Integranden:

r = Riemann_integrate(0, π) do x
        z1 = sin(x)
        z2 = log(1+x)
        if x > 1 
            return z1^2
        else
            return 1/z2^2
        end
    end
1578.9022037353475

10.7 Funktionsartige Objekte

Durch Definition einer geeigneten Methode für einen Typ kann man beliebige Objekte callable machen, d.h., sie anschließend wie Funktionen aufrufen.

# struct speichert die Koeffiziente eines Polynoms 2. Grades
struct Poly2Grad 
    a0::Float64
    a1::Float64
    a2::Float64
end

p1 = Poly2Grad(2,5,1)
p2 = Poly2Grad(3,1,-0.4)
Poly2Grad(3.0, 1.0, -0.4)

Die folgende Methode macht diese Struktur callable.

function (p::Poly2Grad)(x)
    p.a2 * x^2 + p.a1 * x + p.a0
end

Jetzt kann man die Objekte, wenn gewünscht, auch wie Funktionen verwenden.

@show p2(5)  p1(-0.7)  p1;
p2(5) = -2.0
p1(-0.7) = -1.0100000000000002
p1 = Main.Notebook.Poly2Grad(2.0, 5.0, 1.0)

10.8 Operatoren und spezielle Formen

  • Infix-Operatoren wie +,*,>,∈,... sind Funktionen.
+(3, 7)
10
f = +
+ (generic function with 198 methods)
f(3, 7)
10
  • Auch Konstruktionen wie x[i], a.x, [x; y] werden vom Parser zu Funktionsaufrufen umgewandelt.
Spezielle Formen (Auswahl)
x[i] getindex(x, i)
x[i] = z setindex!(x, z, i)
a.x getproperty(a, :x)
a.x = z setproperty!(a, :x, z)
[x; y;…] vcat(x, y, …)

(Der Doppelpunkt vor einer Variablen macht diese zu einem Symbol.)

Hinweis

Für diese Funktionen kann man eigene Methoden implementieren. Zum Beispiel könnten bei einem eigenen Typ das Setzen eines Feldes (setproperty!()) die Gültigkeit des Wertes prüfen oder weitere Aktionen veranlassen.

Prinzipiell können get/setproperty auch Dinge tun, die gar nichts mit einem tatsächlich vorhandenen Feld der Struktur zu tun haben.

10.9 Update-Form

Alle arithmetischen Infix-Operatoren haben eine update-Form: Der Ausdruck

x = x  y

kann auch geschrieben werden als

x ⊙= y

Beide Formen sind semantisch äquivalent. Insbesondere wird in beiden Formen der Variablen x ein auf der rechten Seite geschaffenes neues Objekt zugewiesen.

Ein Speicherplatz- und Zeit-sparendes in-place-update eines Arrays/Vektors/Matrix ist möglich entweder durch explizite Indizierung

for i in eachindex(x)
    x[i] += y[i]
end

oder durch die dazu semantisch äquivalente broadcast-Form (s. Kapitel 12.7):

x .= x .+ y

10.10 Vorrang und Assoziativität von Operatoren

Zu berechnende Ausdrücke

 -2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
false

werden vom Parser in eine Baumstruktur überführt.

using TreeView

walk_tree(Meta.parse("-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2"))

  • Die Auswertung solcher Ausdrücke wird durch
    • Vorrang (precedence) und
    • Assoziativität geregelt.
  • ‘Vorrang’ definiert, welche Operatoren stärker binden im Sinne von “Punktrechnung geht vor Strichrechnung”.
  • ‘Assoziativität’ bestimmt die Auswertungsreihenfolge bei gleichen oder gleichrangigen Operatoren.
  • Vollständige Dokumentation

10.10.1 Assoziativität

Sowohl Addition und Subtraktion als auch Multiplikation und Divison sind jeweils gleichrangig und linksassoziativ, d.h. es wird von links ausgewertet.

200/5/2      # wird von links ausgewertet als (200/5)/2
20.0
200/2*5      # wird von links ausgewertet als (200/2)*5
500.0

Zuweisungen wie =, +=, *=,… sind gleichrangig und rechtsassoziativ.

x = 1
y = 10

# wird von rechts ausgewertet:  x += (y += (z = (a = 20)))

x += y +=  z = a = 20 

@show x y z a;
x = 31
y = 30
z = 20
a = 20

Natürlich kann man die Assoziativität in Julia auch abfragen. Die entsprechenden Funktionen werden nicht explizit aus dem Base-Modul exportiert, deshalb muss man den Modulnamen beim Aufruf angeben.

for i in (:/, :+=, :(=), :^)
    a = Base.operator_associativity(i)
    println("Operation $i is  $(a)-assoziative")
end
Operation / is  left-assoziative
Operation += is  right-assoziative
Operation = is  right-assoziative
Operation ^ is  right-assoziative

Also ist der Potenzoperator rechtsassoziativ.

2^3^2    # rechtsassoziativ,  = 2^(3^2)
512

10.10.2 Vorrang

  • Julia ordnet den Operatoren Vorrangstufen von 1 bis 17 zu:
for i in (:+, :-, :*, :/, :^, :(=))
    p = Base.operator_precedence(i)
    println("Vorrang von  $i = $p") 
end
Vorrang von  + = 11
Vorrang von  - = 11
Vorrang von  * = 12
Vorrang von  / = 12
Vorrang von  ^ = 15
Vorrang von  = = 1
  • 11 ist kleiner als 12, also geht ‘Punktrechnung vor Strichrechnung’
  • Der Potenz-Operator ^ hat eine höhere precedence.
  • Zuweisungen haben die kleinste precedence
#   Zuweisung hat kleinsten Vorrang, daher Auswertung als  x = (3 < 4)

x = 3 < 4     
x
true
(y = 3) < 4   # Klammern schlagen natürlich jeden Vorrang
y
3

Nochmal zum Beispiel vom Anfang von Kapitel 10.10:

-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
false
for i  (:^, :+, :/, :(==), :&&, :>, :|| )
    print(i, " ")
    println(Base.operator_precedence(i))
end
^ 15
+ 11
/ 12
== 7
&& 6
> 7
|| 5

Nach diesen Vorrangregeln wird der Beispielausdruck also wie folgt ausgewertet:

((-(2^3)+((500/2)/10)==8) && (13 > (7 + 1))) || (9 < 2)
false

(Das entspricht natürlich dem oben gezeigten parse-tree)

Es gilt also für den Vorrang:

Potenz > Multiplikation/Division > Addition/Subtraktion > Vergleiche > logisches && > logisches || > Zuweisung

Damit wird ein Ausdruck wie

 a = x <= y + z && x > z/2 

sinnvoll ausgewertet als a = ((x <= (y+z)) && (x < (z/2)))

  • Eine Besonderheit sind noch

    • unäre Operatoren, also insbesondere + und - als Vorzeichen
    • juxtaposition, also Zahlen direkt vor Variablen oder Klammern ohne *-Symbol

    Beide haben Vorrang noch vor Multiplikation und Division.

Wichtig

Damit ändert sich die Bedeutung von Ausdrücken, wenn man juxtaposition anwendet:

1/2*π,  1/2π
(1.5707963267948966, 0.15915494309189535)

Beispiele:

-2^2    # -(2^2)
-4
x = 5
2x^2    # 2(x^2)
50
2^-2    # 2^(-2)
0.25
2^2x  # 2^(2x)
1024
  • Funktionsanwendung f(...) hat Vorrang vor allen Operatoren
sin(x)^2 === (sin(x))^2   # nicht sin(x^2)
true

10.10.3 Zusätzliche Operatoren

Der Julia-Parser definiert für zahlreiche Unicode-Zeichen einen Vorrang auf Vorrat, so dass diese Zeichen von Paketen und selbstgeschriebenem Code als Operatoren benutzt werden können.

So haben z.B.

                             ⦿                               

den Vorrang 12 wie Multiplikation/Division (und sind wie diese linksassoziativ) und z.B.

     |++|    ±                                        

haben den Vorrang 11 wie Addition/Subtraktion.