Files
JuliaKurs23/chapters/9_functs.qmd

641 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 a result, which they return when called.
## Forms
Functions can be defined in different forms:
I. As a `function ... end` block
```{julia}
function hyp(x,y)
sqrt(x^2+y^2)
end
```
II. As a "single-liner"
```{julia}
hyp(x, y) = sqrt(x^2 + y^2)
```
III. As anonymous functions
```{julia}
(x, y) -> sqrt(x^2 + y^2)
```
### Block Form and `return`
- With `return`, function execution is terminated 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 without the second 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 the C world) returns the value `nothing` of type `Nothing`. (Just as an object of type `Bool` can have two values, `true` and `false`, an object of type `Nothing` can only take a single value, namely `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 is a normal assignment where a function stands on the left side.
```julia
hyp(x, y) = sqrt(x^2 + y^2)
```
Julia knows 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 are `map(f, collection)`, which applies a function to all elements of a collection. In Julia, `map(f(x,y), collection1, collection2)` also works:
```{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, no copies are made of the objects passed as function arguments. Variables in the function refer to the original objects. Julia calls this concept _pass_by_sharing_.
- Functions can therefore effectively modify their arguments if they are _mutable_ objects, such as `Vector` or `Array`.
- It is a convention in Julia that the names of such argument-modifying functions end with an exclamation mark. Furthermore, the argument that is modified is usually first and is also the return value of the function.
```{julia}
V = [1, 2, 3]
W = fill!(V, 17)
# '===' is test 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;
```
## Variants of Function Arguments
- 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 value,
2. Positional arguments with default value,
3. --- semicolon ---,
4. comma-separated list of keyword arguments (with or without default value)
- When calling, _keyword_ arguments can appear at any position in any order. You can separate them from positional arguments with a semicolon, but you don't have to.
```{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 Normal Objects
- They can be assigned
```{julia}
f2 = sqrt
f2(2)
```
- They can be passed as arguments to functions.
```{julia}
# very naive numerical 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, π)
```
- 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 anyway local and not available outside. So one can use an anonymous function.
```{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
```
- Of course, you can also 'broadcast' these operators (see @sec-broadcast). Here a vector of functions acts element-wise on 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.
Then you can also call `higherfunc()` without the first argument and instead define the function in an immediately 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 purpose is of course in the application with more complex functions, such as this integrand composed of two parts:
```{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 suitable method for a type, arbitrary objects can be made *callable*, i.e., callable like functions afterwards.
```{julia}
# struct stores the coefficients of a 2nd 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
```
Now the objects can, if desired, also be used like functions.
```{julia}
@show p2(5) p1(-0.7) p1;
```
## Operators and Special Forms
- Infix operators like `+,*,>,∈,...` are functions.
```{julia}
+(3, 7)
```
```{julia}
f = +
```
```{julia}
f(3, 7)
```
- Even 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, one can implement one's own 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. In particular, in both forms, a new object created on the right side is assigned to the variable `x`.
Memory- and time-saving __in-place-update__ of an array/vector/matrix is possible either through explicit indexing
```julia
for i in eachindex(x)
x[i] += y[i]
end
```
or through the semantically equivalent _broadcast_ form (see @sec-broadcast):
```julia
x .= x .+ y
```
## Operator Precedence and Associativity {#sec-vorrang}
Expressions to be computed
```{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"))
```
- The evaluation of such expressions is regulated by
- precedence and
- associativity.
- 'Precedence' defines which operators bind more strongly in the sense of "multiplication and division before addition and subtraction".
- 'Associativity' determines the evaluation order for operators of equal or same rank.
- [Complete documentation](https://docs.julialang.org/en/v1/manual/mathematical-operations/#Operator-Precedence-and-Associativity)
### Associativity
Both addition and subtraction as well as multiplication and division are each of equal rank and left-associative, i.e., they are evaluated from 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;
```
Of course, you can also query the associativity in Julia. The corresponding functions are not explicitly exported from the `Base` module, so the module name must be specified when calling.
```{julia}
for i in (:/, :+=, :(=), :^)
a = Base.operator_associativity(i)
println("Operation $i is $(a)-associative")
end
```
So 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
```
- 11 is less than 12, so 'multiplication and division before addition and subtraction'
- The power operator `^` has a higher _precedence_.
- Assignments have the smallest _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
```
Once more to the example from the beginning of @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
```
According to these precedence rules, the example expression is evaluated as follows:
```{julia}
((-(2^3)+((500/2)/10)==8) && (13 > (7 + 1))) || (9 < 2)
```
(This of course corresponds to the *parse-tree* shown 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.