--- 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.