Files
JuliaKurs23/chapters/9_functs.qmd
2026-03-05 20:09:16 +01:00

640 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
engine: julia
---
# Functions and Operators
```{julia}
#| error: false
#| echo: false
#| output: false
using InteractiveUtils
import QuartoNotebookWorker
Base.stdout = QuartoNotebookWorker.with_context(stdout)
myactive_module() = Main.Notebook
Base.active_module() = myactive_module()
# https://github.com/JuliaLang/julia/blob/master/base/show.jl#L516-L520
# https://github.com/JuliaLang/julia/blob/master/base/show.jl#L3073-L3077
```
Functions process their arguments to produce and return a result when called.
## Forms of function definitions
I. Block form: `function ... end`
```{julia}
function hyp(x,y)
sqrt(x^2+y^2)
end
```
II. Single-line form
```{julia}
hyp(x, y) = sqrt(x^2 + y^2)
```
III. Anonymous functions
```{julia}
(x, y) -> sqrt(x^2 + y^2)
```
### Block Form and `return` Statement
- With `return`, function execution terminates and control returns to the calling context.
- Without `return`, the value of the last expression is returned as the function value.
The two definitions
```julia
function xsinrecipx(x)
if x == 0
return 0.0
end
return x * sin(1/x)
end
```
and the equivalent version without explicit `return` in the last line:
```julia
function xsinrecipx(x)
if x == 0
return 0.0
end
x * sin(1/x)
end
```
are therefore equivalent.
- A function that returns `nothing` (_void functions_ in C) returns a `nothing` value of type `Nothing`. (Just as a `Bool` object has two values, `true` and `false`, a `Nothing` object has only one: `nothing`.)
- An empty `return` statement is equivalent to `return nothing`.
```{julia}
function fn(x)
println(x)
return
end
a = fn(2)
```
```{julia}
a
```
```{julia}
@show a typeof(a);
```
### Single-liner Form
The single-liner form looks like a simple assignment:
```julia
hyp(x, y) = sqrt(x^2 + y^2)
```
Julia provides two ways to combine multiple statements into a block that can stand in place of a single statement:
- `begin ... end` block
- Parenthesized statements separated by semicolons.
In both cases, the value of the block is the value of the last statement.
Thus, the following also works:
```julia
hyp(x, y) = (z = x^2; z += y^2; sqrt(z))
```
and
```julia
hyp(x, y) = begin
z = x^2
z += y^2
sqrt(z)
end
```
### Anonymous Functions
Anonymous functions can be "rescued from anonymity" by assigning them a name:
```julia
hyp = (x,y) -> sqrt(x^2 + y^2)
```
Their actual application is in calling a *(higher order)* function that expects a function as an argument.
Typical applications include `map(f, collection)`, which applies a function to every element of a collection. Julia also supports `map(f, collection1, collection2)` with multiple collections:
```{julia}
map( (x,y) -> sqrt(x^2 + y^2), [3, 5, 8], [4, 12, 15])
```
```{julia}
map( x->3x^3, 1:8 )
```
Another example is `filter(test, collection)`, where a test is a function that returns a `Bool`.
```{julia}
filter(x -> ( x%3 == 0 && x%5 == 0), 1:100 )
```
## Argument Passing
- When calling a function, Julia does not copy objects passed as arguments. Function arguments refer to the original objects. Julia calls this concept _pass_by_sharing_.
- Consequently, functions can modify their arguments if they are mutable (e.g., `Vector` or `Array`).
- By convention, functions that modify their arguments end with an exclamation mark. The modified argument is typically the first argument and is also returned.
```{julia}
V = [1, 2, 3]
W = fill!(V, 17)
# '===' tests for identity
@show V W V===W; # V and W refer to the same object
```
```{julia}
function fill_first!(V, x)
V[1] = x
return V
end
U = fill_first!(V, 42)
@show V U V===U;
```
## Function Argument Variants
- There are positional arguments (1st argument, 2nd argument, ...) and _keyword_ arguments, which must be addressed by name when calling.
- Both positional and _keyword_ arguments can have _default_ values. These arguments can be omitted when calling.
- The order of declaration must be:
1. Positional arguments without default values,
2. Positional arguments with default values,
3. --- semicolon ---,
4. comma-separated list of keyword arguments (with or without default values)
- When calling, keyword arguments can appear in any order at any position. They can be separated from positional arguments with a semicolon, but this is optional.
```{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)
```
A function with only _keyword_ arguments is declared as follows:
```{julia}
fkw(; x=10, y) = println("x=$x, y=$y")
fkw(y=2)
```
## Functions are just Objects
- Functions can be assigned to variables
```{julia}
f2 = sqrt
f2(2)
```
- Functions can be passed as arguments to other functions.
```{julia}
# naive Riemann integration example
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, π)
```
- They can be created by functions and returned as results.
```{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)
```
The above function `generate_add_func()` can also be defined more briefly. The inner function name `addx` is local and inaccessible outside. An anonymous function can be used instead.
```{julia}
generate_add_func(x) = y -> x + y
```
## Function Composition: the Operators $\circ$ and `|>`
- Function composition can also be written with the $\circ$ operator (`\circ + Tab`)
$$(f\circ g)(x) = f(g(x))$$
```{julia}
(sqrt ∘ + )(9, 16)
```
```{julia}
f = cos ∘ sin ∘ (x->2x)
f(.2)
```
```{julia}
@show map(uppercase ∘ first, ["one", "a", "green", "leaves"]);
```
- There is also an operator with which functions can act "from the right" and be composed (_piping_)
```{julia}
25 |> sqrt
```
```{julia}
1:10 |> sum |> sqrt
```
- These operators can also be broadcast (see @sec-broadcast). A vector of functions is applied element-wise to a vector of arguments:
```{julia}
["a", "list", "of", "strings"] .|> [length, uppercase, reverse, titlecase]
```
## The `do` Notation {#sec-do}
A syntactic peculiarity for defining anonymous functions as arguments of other functions is the `do` notation.
Let `higherfunc(f, a, ...)` be a function whose first argument is a function.
The function can be called without the first argument, with the function body defined in a following `do` block:
```julia
higherfunc(a, b) do x, y
body of f(x,y)
end
```
Using `Riemann_integrate()` as an example, this looks like this:
```{julia}
# this is the same as Riemann_integrate(x->x^2, 0, 2)
Riemann_integrate(0, 2) do x x^2 end
```
The `do` notation is especially useful for complex function bodies, such as this integrand defined in multiple steps:
```{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
```
## Function-like Objects
By defining a method for a type, objects become *callable* like functions.
```{julia}
# struct stores coefficients of a second-degree polynomial
struct Poly2Grad
a0::Float64
a1::Float64
a2::Float64
end
p1 = Poly2Grad(2,5,1)
p2 = Poly2Grad(3,1,-0.4)
```
The following method makes this structure callable:
```{julia}
function (p::Poly2Grad)(x)
p.a2 * x^2 + p.a1 * x + p.a0
end
```
Objects can now be used like functions:
```{julia}
@show p2(5) p1(-0.7) p1;
```
## Operators and Special Forms
- Infix operators such as `+`, `*`, `>`, `∈` are functions.
```{julia}
+(3, 7)
```
```{julia}
f = +
```
```{julia}
f(3, 7)
```
- Constructions like `x[i]`, `a.x`, `[x; y]` are converted by the parser to function calls.
:::{.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, ...) |
:Special Forms [(selection)](https://docs.julialang.org/en/v1/manual/functions/#Operators-With-Special-Names)
:::
(The colon before a variable makes it into a symbol.)
:::{.callout-note}
For these functions, too, van be extended/overwritten by new methods. For example, for a custom type, setting a field (`setproperty!()`) could check the validity of the value or trigger further actions.
In principle, `get/setproperty` can also do things that have nothing to do with an actually existing field of the structure.
:::
## Update Form
All arithmetic infix operators have an update form: The expression
```julia
x = x ⊙ y
```
can also be written as
```julia
x ⊙= y
```
Both forms are semantically equivalent: a new object created on the right is assigned to `x`.
Memory- and time-efficient *in-place updates* of arrays use explicit indexing:
```julia
for i in eachindex(x)
x[i] += y[i]
end
```
or semantically equivalent broadcast form (see @sec-broadcast):
```julia
x .= x .+ y
```
## Operator Precedence and Associativity {#sec-vorrang}
Expressions like
```{julia}
-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
```
are converted by the parser into a tree structure:
```{julia}
using TreeView
walk_tree(Meta.parse("-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2"))
```
- Expression evaluation is governed by
- precedence and
- associativity.
- Precedence determines which operators bind more tightly, such as multiplication before addition.
- Associativity determines the evaluation order for operators of equal precedence.
- [Complete documentation](https://docs.julialang.org/en/v1/manual/mathematical-operations/#Operator-Precedence-and-Associativity)
### Associativity
Addition/subtraction and multiplication/division have equal precedence and are left-associative (evaluated left-to-right):
```{julia}
200/5/2 # evaluated left to right as (200/5)/2
```
```{julia}
200/2*5 # evaluated left to right as (200/2)*5
```
Assignments like `=`, `+=`, `*=`,... are of equal rank and right-associative.
```{julia}
x = 1
y = 10
# evaluated right to left: x += (y += (z = (a = 20)))
x += y += z = a = 20
@show x y z a;
```
Julia provides functions to query associativity. These functions are not exported from `Base`, so the module name must be specified.
```{julia}
for i in (:/, :+=, :(=), :^)
a = Base.operator_associativity(i)
println("Operation $i is $(a)-associative")
end
```
Thus, the power operator is right-associative:
```{julia}
2^3^2 # right-associative, = 2^(3^2)
```
### Precedence
- Julia assigns operator precedence levels from 1 to 17:
```{julia}
for i in (:+, :-, :*, :/, :^, :(=))
p = Base.operator_precedence(i)
println("Precedence of $i = $p")
end
```
- Precedence 11 < 12 explains why multiplication/division bind tighter than addition/subtraction.
- The power operator `^` has higher precedence.
- Assignments have the lowest precedence.
```{julia}
# assignment has smallest precedence, therefore evaluation as x = (3 < 4)
x = 3 < 4
x
```
```{julia}
(y = 3) < 4 # parentheses override any precedence
y
```
Returning to the example above:
```{julia}
-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
```
```{julia}
for i ∈ (:^, :+, :/, :(==), :&&, :>, :|| )
print(i, " ")
println(Base.operator_precedence(i))
end
```
These rules evaluate the expression as:
```{julia}
((-(2^3)+((500/2)/10)==8) && (13 > (7 + 1))) || (9 < 2)
```
(as shown in the parse tree above).
So the precedence is:
> Power > Multiplication/Division > Addition/Subtraction > Comparisons > logical && > logical || > assignment
Thus, an expression like
```julia
a = x <= y + z && x > z/2
```
is sensibly evaluated as `a = ((x <= (y+z)) && (x < (z/2)))`
- A special case is still
- unary operators, in particular `+` and `-` as signs
- _juxtaposition_, i.e., numbers directly before variables or parentheses without `*` symbol
Both have precedence even before multiplication and division.
:::{.callout-important}
Therefore, the meaning of expressions changes when one applies _juxtaposition_:
```{julia}
1/2*π, 1/2π
```
:::
- Compared to the power operator `^` (see [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.
Examples:
```{julia}
-2^2 # -(2^2)
```
```{julia}
x = 5
2x^2 # 2(x^2)
```
```{julia}
2^-2 # 2^(-2)
```
```{julia}
2^2x # 2^(2x)
```
- Function application `f(...)` has precedence over all operators
```{julia}
sin(x)^2 === (sin(x))^2 # not sin(x^2)
```
### Additional Operators
The [Julia parser](https://github.com/JuliaLang/julia/blob/master/src/julia-parser.scm#L13-L31) assigns precedence to numerous Unicode characters in advance, so that these characters can be used as operators by packages and self-written code.
Thus, for example,
```julia
∧ ⊗ ⊘ ⊙ ⊚ ⊛ ⊠ ⊡ ⊓ ∙ ∤ ⅋ ≀ ⊼ ⋄ ⋆ ⋇ ⋉ ⋊ ⋋ ⋌ ⋏ ⋒ ⟑ ⦸ ⦼ ⦾ ⦿ ⧶ ⧷ ⨇ ⨰ ⨱ ⨲ ⨳ ⨴ ⨵ ⨶ ⨷ ⨸ ⨻ ⨼ ⨽ ⩀ ⩃ ⩄ ⩋ ⩍ ⩎ ⩑ ⩓ ⩕ ⩘ ⩚ ⩜ ⩞ ⩟ ⩠ ⫛
```
have precedence 12 like multiplication/division (and are left-associative like these)
and for example
```julia
⊕ ⊖ ⊞ ⊟ |++| ⊔ ± ∓ ∔ ∸ ≏ ⊎ ⊻ ⊽ ⋎ ⋓ ⧺ ⧻ ⨈ ⨢ ⨣ ⨤ ⨥ ⨦ ⨧ ⨨ ⨩ ⨪ ⨫ ⨬ ⨭ ⨮ ⨹ ⨺ ⩁ ⩂ ⩅ ⩊ ⩌ ⩏ ⩐ ⩒ ⩔ ⩖ ⩗
```
have precedence 11 like addition/subtraction.