10  Functions and Operators

Functions process their arguments to produce and return a result when called.

10.1 Forms of function definitions

I. Block form: function ... end

function hyp(x,y)
    sqrt(x^2+y^2)
end
hyp (generic function with 1 method)
  1. Single-line form
hyp(x, y) = sqrt(x^2 + y^2)
hyp (generic function with 1 method)
  1. Anonymous functions
(x, y) -> sqrt(x^2 + y^2)
#2 (generic function with 1 method)

10.1.1 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

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:

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.
function fn(x)
    println(x)
    return 
end

a = fn(2)
2
a
@show a typeof(a);
a = nothing
typeof(a) = Nothing

10.1.2 Single-liner Form

The single-liner form looks like a simple assignment:

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:

hyp(x, y) = (z = x^2; z += y^2; sqrt(z))

and

hyp(x, y) = begin 
                z = x^2
                z += y^2 
                sqrt(z) 
            end

10.1.3 Anonymous Functions

Anonymous functions can be “rescued from anonymity” by assigning them a name:

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:

map( (x,y) -> sqrt(x^2 + y^2), [3, 5, 8], [4, 12, 15])
3-element Vector{Float64}:
  5.0
 13.0
 17.0
map( x->3x^3, 1:8 )
8-element Vector{Int64}:
    3
   24
   81
  192
  375
  648
 1029
 1536

Another example is filter(test, collection), where a test is a function that returns a Bool.

filter(x -> ( x%3 == 0 && x%5 == 0), 1:100  )
6-element Vector{Int64}:
 15
 30
 45
 60
 75
 90

10.2 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.
V = [1, 2, 3]

W = fill!(V, 17)
                        # '===' tests for identity
@show  V  W  V===W;     # V and W refer to the same object
V = [17, 17, 17]
W = [17, 17, 17]
V === W = true
function fill_first!(V, x)
        V[1] = x
        return V    
end                     

U = fill_first!(V, 42)

@show V   U   V===U;
V = [42, 17, 17]
U = [42, 17, 17]
V === U = true

10.3 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.
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)
x=6, y=7, a=4
x=6, y=7, a=4
x=6, y=42, a=-2

A function with only keyword arguments is declared as follows:

fkw(; x=10, y) = println("x=$x, y=$y")

fkw(y=2)
x=10, y=2

10.4 Functions are just Objects

  • Functions can be assigned to variables
f2 = sqrt
f2(2)
1.4142135623730951
  • Functions can be passed as arguments to other functions.
# 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, π)
2.0000008224672694
  • They can be created by functions and returned as results.
function generate_add_func(x)
    function addx(y)
        return x+y
    end
    return addx
end
generate_add_func (generic function with 1 method)
h =  generate_add_func(4)
(::Main.Notebook.var"#addx#generate_add_func##0"{Int64}) (generic function with 1 method)
h(1)
5
h(2), h(10)
(6, 14)

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.

generate_add_func(x) = y -> x + y
generate_add_func (generic function with 1 method)

10.5 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))\]

(sqrt  + )(9, 16)
5.0
f = cos  sin  (x->2x)
f(.2)
0.9251300429004277
@show map(uppercase  first, ["one", "a", "green", "leaves"]);
map(uppercase ∘ first, ["one", "a", "green", "leaves"]) = ['O', 'A', 'G', 'L']
  • There is also an operator with which functions can act “from the right” and be composed (piping)
25 |> sqrt
5.0
1:10 |> sum |> sqrt
7.416198487095663
  • These operators can also be broadcast (see Section 12.7). A vector of functions is applied element-wise to a vector of arguments:
["a", "list", "of", "strings"] .|> [length, uppercase, reverse, titlecase]
4-element Vector{Any}:
 1
  "LIST"
  "fo"
  "Strings"

10.6 The do Notation

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:

higherfunc(a, b) do x, y
   body of f(x,y)
end

Using Riemann_integrate() as an example, this looks like this:

# this is the same as Riemann_integrate(x->x^2, 0, 2)

Riemann_integrate(0, 2) do x x^2 end
2.6666659999999993

The do notation is especially useful for complex function bodies, such as this integrand defined in multiple steps:

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
1578.9022037353475

10.7 Function-like Objects

By defining a method for a type, objects become callable like functions.

# 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)
Poly2Grad(3.0, 1.0, -0.4)

The following method makes this structure callable:

function (p::Poly2Grad)(x)
    p.a2 * x^2 + p.a1 * x + p.a0
end

Objects can now be used like functions:

@show p2(5)  p1(-0.7)  p1;
p2(5) = -2.0
p1(-0.7) = -1.0100000000000002
p1 = Poly2Grad(2.0, 5.0, 1.0)

10.8 Operators and Special Forms

  • Infix operators such as +, *, >, are functions.
+(3, 7)
10
f = +
+ (generic function with 191 methods)
f(3, 7)
10
  • Constructions like x[i], a.x, [x; y] are converted by the parser to function calls.
Special Forms (selection)
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, …)

(The colon before a variable makes it into a symbol.)

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.

10.9 Update Form

All arithmetic infix operators have an update form: The expression

x = x  y

can also be written as

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:

for i in eachindex(x)
    x[i] += y[i]
end

or semantically equivalent broadcast form (see Section 12.7):

x .= x .+ y

10.10 Operator Precedence and Associativity

Expressions like

 -2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
false

are converted by the parser into a tree structure:

using TreeView

walk_tree(Meta.parse("-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2"))

Error showing value of type TreeView.LabelledTree
StackOverflowError:
Stacktrace:
 [1] show(io::IOContext{IOBuffer}, x::LabelledTree) (repeats 79983 times)
   @ Main.Notebook ~/Julia/Book26/JuliaBook/chapters/9_functs.qmd:26
  • 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

10.10.1 Associativity

Addition/subtraction and multiplication/division have equal precedence and are left-associative (evaluated left-to-right):

200/5/2      # evaluated left to right as (200/5)/2
20.0
200/2*5      # evaluated left to right as (200/2)*5
500.0

Assignments like =, +=, *=,… are of equal rank and right-associative.

x = 1
y = 10

# evaluated right to left:  x += (y += (z = (a = 20)))

x += y +=  z = a = 20 

@show x y z a;
x = 31
y = 30
z = 20
a = 20

Julia provides functions to query associativity. These functions are not exported from Base, so the module name must be specified.

for i in (:/, :+=, :(=), :^)
    a = Base.operator_associativity(i)
    println("Operation $i is  $(a)-associative")
end
Operation / is  left-associative
Operation += is  right-associative
Operation = is  right-associative
Operation ^ is  right-associative

Thus, the power operator is right-associative:

2^3^2    # right-associative,  = 2^(3^2)
512

10.10.2 Precedence

  • Julia assigns operator precedence levels from 1 to 17:
for i in (:+, :-, :*, :/, :^, :(=))
    p = Base.operator_precedence(i)
    println("Precedence of  $i = $p") 
end
Precedence of  + = 11
Precedence of  - = 11
Precedence of  * = 12
Precedence of  / = 12
Precedence of  ^ = 15
Precedence of  = = 1
  • Precedence 11 < 12 explains why multiplication/division bind tighter than addition/subtraction.
  • The power operator ^ has higher precedence.
  • Assignments have the lowest precedence.
#   assignment has smallest precedence, therefore evaluation as  x = (3 < 4)

x = 3 < 4     
x
true
(y = 3) < 4   # parentheses override any precedence
y
3

Returning to the example above:

-2^3+500/2/10==8 && 13 > 7 + 1 || 9 < 2
false
for i  (:^, :+, :/, :(==), :&&, :>, :|| )
    print(i, " ")
    println(Base.operator_precedence(i))
end
^ 15
+ 11
/ 12
== 7
&& 6
> 7
|| 5

These rules evaluate the expression as:

((-(2^3)+((500/2)/10)==8) && (13 > (7 + 1))) || (9 < 2)
false

(as shown in the parse tree above).

So the precedence is:

Power > Multiplication/Division > Addition/Subtraction > Comparisons > logical && > logical || > assignment

Thus, an expression like

 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.

Important

Therefore, the meaning of expressions changes when one applies juxtaposition:

1/2*π,  1/2π
(1.5707963267948966, 0.15915494309189535)

Examples:

-2^2    # -(2^2)
-4
x = 5
2x^2    # 2(x^2)
50
2^-2    # 2^(-2)
0.25
2^2x  # 2^(2x)
1024
  • Function application f(...) has precedence over all operators
sin(x)^2 === (sin(x))^2   # not sin(x^2)
true

10.10.3 Additional Operators

The Julia parser 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,

                             ⦿                               

have precedence 12 like multiplication/division (and are left-associative like these) and for example

      |++|    ±                                        

have precedence 11 like addition/subtraction.