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