641 lines
14 KiB
Plaintext
641 lines
14 KiB
Plaintext
---
|
||
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.
|
||
|