636 lines
15 KiB
Plaintext
636 lines
15 KiB
Plaintext
---
|
||
engine: julia
|
||
---
|
||
|
||
```{julia}
|
||
#| error: false
|
||
#| echo: false
|
||
#| output: false
|
||
using InteractiveUtils
|
||
```
|
||
|
||
# Funktionen und Operatoren
|
||
|
||
Funktionen verarbeiten ihre Argumente zu einem Ergebnis, das sie beim Aufruf zurückliefern.
|
||
|
||
## Formen
|
||
|
||
Funktionen können in verschiedenen Formen definiert werden:
|
||
|
||
I. Als `function ... end`-Block
|
||
```{julia}
|
||
function hyp(x,y)
|
||
sqrt(x^2+y^2)
|
||
end
|
||
```
|
||
II. Als "Einzeiler"
|
||
```{julia}
|
||
hyp(x, y) = sqrt(x^2 + y^2)
|
||
```
|
||
III. Als anonyme Funktionen
|
||
|
||
```{julia}
|
||
(x, y) -> sqrt(x^2 + y^2)
|
||
```
|
||
|
||
|
||
### 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
|
||
|
||
```julia
|
||
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:
|
||
|
||
```julia
|
||
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`.
|
||
|
||
|
||
```{julia}
|
||
function fn(x)
|
||
println(x)
|
||
return
|
||
end
|
||
|
||
a = fn(2)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
a
|
||
```
|
||
|
||
|
||
```{julia}
|
||
@show a typeof(a);
|
||
```
|
||
|
||
|
||
### Einzeiler-Form
|
||
|
||
Die Einzeilerform ist eine ganz normale Zuweisung, bei der links eine Funktion steht.
|
||
|
||
```julia
|
||
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
|
||
```julia
|
||
hyp(x, y) = (z = x^2; z += y^2; sqrt(z))
|
||
```
|
||
|
||
und
|
||
```julia
|
||
hyp(x, y) = begin
|
||
z = x^2
|
||
z += y^2
|
||
sqrt(z)
|
||
end
|
||
```
|
||
|
||
### Anonyme Funktionen
|
||
|
||
Anonyme FUnktionen kann man der Anonymität entreisen, indem man ihnen einen Namen zuweist.
|
||
```julia
|
||
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)`:
|
||
|
||
```{julia}
|
||
map( (x,y) -> sqrt(x^2 + y^2), [3, 5, 8], [4, 12, 15])
|
||
```
|
||
|
||
```{julia}
|
||
map( x->3x^3, 1:8 )
|
||
```
|
||
|
||
Ein weiteres Beispiel ist `filter(test, collection)`, wobei ein Test eine Funktion ist, die ein `Bool` zurückgibt.
|
||
```{julia}
|
||
filter(x -> ( x%3 == 0 && x%5 == 0), 1:100 )
|
||
```
|
||
|
||
## 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.
|
||
|
||
```{julia}
|
||
V = [1, 2, 3]
|
||
|
||
W = fill!(V, 17)
|
||
# '===' ist Test auf Identität
|
||
@show V W V===W; # V und W benennen dasselbe Objekt
|
||
```
|
||
|
||
```{julia}
|
||
function fill_first!(V, x)
|
||
V[1] = x
|
||
return V
|
||
end
|
||
|
||
U = fill_first!(V, 42)
|
||
|
||
@show V U V===U;
|
||
```
|
||
|
||
|
||
|
||
## 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.
|
||
|
||
```{julia}
|
||
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)
|
||
```
|
||
|
||
Eine Funktion nur mit _keyword_-Argumenten wird so deklariert:
|
||
|
||
```{julia}
|
||
fkw(; x=10, y) = println("x=$x, y=$y")
|
||
|
||
fkw(y=2)
|
||
```
|
||
|
||
|
||
|
||
## Funktionen sind ganz normale Objekte
|
||
|
||
- Sie können zugewiesen werden
|
||
|
||
|
||
```{julia}
|
||
f2 = sqrt
|
||
f2(2)
|
||
```
|
||
|
||
|
||
- Sie können als Argumente an Funktionen übergeben werden.
|
||
|
||
|
||
```{julia}
|
||
# 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, π)
|
||
```
|
||
|
||
|
||
- Sie können von Funktionen erzeugt und als Ergebnis `return`t werden.
|
||
|
||
|
||
|
||
```{julia}
|
||
function generate_add_func(x)
|
||
function addx(y)
|
||
return x+y
|
||
end
|
||
return addx
|
||
end
|
||
```
|
||
|
||
|
||
```{julia}
|
||
h = generate_add_func(4)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
h(1)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
h(2), h(10)
|
||
```
|
||
|
||
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.
|
||
|
||
```{julia}
|
||
generate_add_func(x) = y -> x + y
|
||
```
|
||
|
||
|
||
## 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))$$
|
||
|
||
|
||
```{julia}
|
||
(sqrt ∘ + )(9, 16)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
f = cos ∘ sin ∘ (x->2x)
|
||
f(.2)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
@show map(uppercase ∘ first, ["ein", "paar", "grüne", "Blätter"]);
|
||
```
|
||
|
||
|
||
- Es gibt auch einen Operator, mit dem Funktionen "von rechts" wirken und zusammengesetzt werden können _(piping)_
|
||
|
||
|
||
```{julia}
|
||
25 |> sqrt
|
||
```
|
||
|
||
|
||
```{julia}
|
||
1:10 |> sum |> sqrt
|
||
```
|
||
|
||
|
||
- Natürlich kann man auch diese Operatoren 'broadcasten' (s. @sec-broadcast). Hier wirkt ein Vektor von Funktionen elementweise auf einen Vektor von Argumenten:
|
||
|
||
```{julia}
|
||
["a", "list", "of", "strings"] .|> [length, uppercase, reverse, titlecase]
|
||
```
|
||
|
||
## 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:
|
||
|
||
```julia
|
||
higherfunc(a, b) do x, y
|
||
Körper von f(x,y)
|
||
end
|
||
```
|
||
|
||
Am Beispiel von `Riemann_integrate()` sieht das so aus:
|
||
|
||
|
||
```{julia}
|
||
# das ist dasselbe wie Riemann_integrate(x->x^2, 0, 2)
|
||
|
||
Riemann_integrate(0, 2) do x x^2 end
|
||
```
|
||
|
||
|
||
Der Sinn besteht natürlich in der Anwendung mit komplexeren Funktionen, wie diesem aus zwei Teilstücken zusammengesetzten Integranden:
|
||
```{julia}
|
||
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
|
||
```
|
||
|
||
## 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.
|
||
|
||
```{julia}
|
||
# 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)
|
||
```
|
||
|
||
Die folgende Methode macht diese Struktur `callable`.
|
||
```{julia}
|
||
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.
|
||
|
||
```{julia}
|
||
@show p2(5) p1(-0.7) p1;
|
||
```
|
||
|
||
|
||
|
||
## Operatoren und spezielle Formen
|
||
|
||
- Infix-Operatoren wie `+,*,>,∈,...` sind Funktionen.
|
||
|
||
|
||
|
||
```{julia}
|
||
+(3, 7)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
f = +
|
||
```
|
||
|
||
|
||
```{julia}
|
||
f(3, 7)
|
||
```
|
||
|
||
- Auch Konstruktionen wie `x[i]`, `a.x`, `[x; y]` werden vom Parser zu Funktionsaufrufen umgewandelt.
|
||
|
||
:::{.narrow}
|
||
|
||
| | |
|
||
| :-: | :------------ |
|
||
| 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, ...) |
|
||
:Spezielle Formen [(Auswahl)](https://docs.julialang.org/en/v1/manual/functions/#Operators-With-Special-Names)
|
||
|
||
:::
|
||
|
||
(Der Doppelpunkt vor einer Variablen macht diese zu einem Symbol.)
|
||
|
||
:::{.callout-note}
|
||
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.
|
||
:::
|
||
|
||
## Update-Form
|
||
|
||
Alle arithmetischen Infix-Operatoren haben eine update-Form: Der Ausdruck
|
||
```julia
|
||
x = x ⊙ y
|
||
```
|
||
kann auch geschrieben werden als
|
||
```julia
|
||
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
|
||
```julia
|
||
for i in eachindex(x)
|
||
x[i] += y[i]
|
||
end
|
||
```
|
||
oder durch die dazu semantisch äquivalente _broadcast_-Form (s. @sec-broadcast):
|
||
```julia
|
||
x .= x .+ y
|
||
```
|
||
|
||
|
||
|
||
## Vorrang und Assoziativität von Operatoren {#sec-vorrang}
|
||
|
||
Zu berechnende Ausdrücke
|
||
|
||
```{julia}
|
||
-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
|
||
```
|
||
|
||
werden vom Parser in eine Baumstruktur überführt.
|
||
```{julia}
|
||
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](https://docs.julialang.org/en/v1/manual/mathematical-operations/#Operator-Precedence-and-Associativity)
|
||
|
||
### Assoziativität
|
||
|
||
Sowohl Addition und Subtraktion als auch Multiplikation und Divison sind jeweils gleichrangig und linksassoziativ, d.h. es wird von links ausgewertet.
|
||
|
||
```{julia}
|
||
200/5/2 # wird von links ausgewertet als (200/5)/2
|
||
```
|
||
|
||
|
||
```{julia}
|
||
200/2*5 # wird von links ausgewertet als (200/2)*5
|
||
```
|
||
|
||
Zuweisungen wie `=`, `+=`, `*=`,... sind gleichrangig und rechtsassoziativ.
|
||
|
||
```{julia}
|
||
x = 1
|
||
y = 10
|
||
|
||
# wird von rechts ausgewertet: x += (y += (z = (a = 20)))
|
||
|
||
x += y += z = a = 20
|
||
|
||
@show x y z a;
|
||
```
|
||
|
||
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.
|
||
|
||
|
||
```{julia}
|
||
for i in (:/, :+=, :(=), :^)
|
||
a = Base.operator_associativity(i)
|
||
println("Operation $i is $(a)-assoziative")
|
||
end
|
||
```
|
||
|
||
Also ist der Potenzoperator rechtsassoziativ.
|
||
```{julia}
|
||
2^3^2 # rechtsassoziativ, = 2^(3^2)
|
||
```
|
||
|
||
### Vorrang
|
||
|
||
- Julia ordnet den Operatoren Vorrangstufen von 1 bis 17 zu:
|
||
|
||
|
||
|
||
```{julia}
|
||
for i in (:+, :-, :*, :/, :^, :(=))
|
||
p = Base.operator_precedence(i)
|
||
println("Vorrang von $i = $p")
|
||
end
|
||
```
|
||
|
||
|
||
- 11 ist kleiner als 12, also geht 'Punktrechnung vor Strichrechnung'
|
||
- Der Potenz-Operator `^` hat eine höhere _precedence_.
|
||
- Zuweisungen haben die kleinste _precedence_
|
||
|
||
|
||
```{julia}
|
||
# Zuweisung hat kleinsten Vorrang, daher Auswertung als x = (3 < 4)
|
||
|
||
x = 3 < 4
|
||
x
|
||
```
|
||
|
||
|
||
```{julia}
|
||
(y = 3) < 4 # Klammern schlagen natürlich jeden Vorrang
|
||
y
|
||
```
|
||
|
||
Nochmal zum Beispiel vom Anfang von @sec-vorrang:
|
||
|
||
```{julia}
|
||
-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
|
||
```
|
||
|
||
|
||
```{julia}
|
||
for i ∈ (:^, :+, :/, :(==), :&&, :>, :|| )
|
||
print(i, " ")
|
||
println(Base.operator_precedence(i))
|
||
end
|
||
```
|
||
Nach diesen Vorrangregeln wird der Beispielausdruck also wie folgt ausgewertet:
|
||
|
||
```{julia}
|
||
((-(2^3)+((500/2)/10)==8) && (13 > (7 + 1))) || (9 < 2)
|
||
```
|
||
(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
|
||
```julia
|
||
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.
|
||
|
||
:::{.callout-important}
|
||
Damit ändert sich die Bedeutung von Ausdrücken, wenn man _juxtaposition_ anwendet:
|
||
```{julia}
|
||
1/2*π, 1/2π
|
||
```
|
||
:::
|
||
|
||
- Im Vergleich zum Potenzoperator `^` gilt (s. [https://discourse.julialang.org/t/confused-about-operator-precedence-for-2-3x/8214/7](https://discourse.julialang.org/t/confused-about-operator-precedence-for-2-3x/8214/7) ):
|
||
|
||
> Unary operators, including juxtaposition, bind tighter than ^ on the right but looser on the left.
|
||
|
||
Beispiele:
|
||
```{julia}
|
||
-2^2 # -(2^2)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
x = 5
|
||
2x^2 # 2(x^2)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
2^-2 # 2^(-2)
|
||
```
|
||
|
||
|
||
```{julia}
|
||
2^2x # 2^(2x)
|
||
```
|
||
|
||
|
||
- Funktionsanwendung `f(...)` hat Vorrang vor allen Operatoren
|
||
|
||
|
||
```{julia}
|
||
sin(x)^2 === (sin(x))^2 # nicht sin(x^2)
|
||
```
|
||
|
||
### Zusätzliche Operatoren
|
||
|
||
Der [Julia-Parser](https://github.com/JuliaLang/julia/blob/master/src/julia-parser.scm#L13-L31) 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.
|
||
```julia
|
||
∧ ⊗ ⊘ ⊙ ⊚ ⊛ ⊠ ⊡ ⊓ ∗ ∙ ∤ ⅋ ≀ ⊼ ⋄ ⋆ ⋇ ⋉ ⋊ ⋋ ⋌ ⋏ ⋒ ⟑ ⦸ ⦼ ⦾ ⦿ ⧶ ⧷ ⨇ ⨰ ⨱ ⨲ ⨳ ⨴ ⨵ ⨶ ⨷ ⨸ ⨻ ⨼ ⨽ ⩀ ⩃ ⩄ ⩋ ⩍ ⩎ ⩑ ⩓ ⩕ ⩘ ⩚ ⩜ ⩞ ⩟ ⩠ ⫛
|
||
```
|
||
|
||
den Vorrang 12 wie Multiplikation/Division (und sind wie diese linksassoziativ)
|
||
und z.B.
|
||
```julia
|
||
⊕ ⊖ ⊞ ⊟ |++| ∪ ∨ ⊔ ± ∓ ∔ ∸ ≏ ⊎ ⊻ ⊽ ⋎ ⋓ ⧺ ⧻ ⨈ ⨢ ⨣ ⨤ ⨥ ⨦ ⨧ ⨨ ⨩ ⨪ ⨫ ⨬ ⨭ ⨮ ⨹ ⨺ ⩁ ⩂ ⩅ ⩊ ⩌ ⩏ ⩐ ⩒ ⩔ ⩖ ⩗
|
||
```
|
||
haben den Vorrang 11 wie Addition/Subtraktion.
|
||
|
||
|
||
|