Videos related to this segment (click the title to watch)
01 JULIA1 - 4A: Variables scope (9:47)
01 JULIA1 - 4B: Loops and conditional statements (9:2)
01 JULIA1 - 4C: Functions (25:57)

0104 Control Flow and Functions

Some stuff to set-up the environment..

julia> cd(@__DIR__)
julia> using Pkg
julia> Pkg.activate(".") # If using a Julia version different than 1.10 please uncomment and run the following line (reproductibility guarantee will hower be lost) # Pkg.resolve() # Pkg.instantiate() # run this if you didn't in Segment 01.01 Activating project at `~/work/SPMLJ/SPMLJ/buildedDoc/01_-_JULIA1_-_Basic_Julia_programming`
julia> using Random
julia> Random.seed!(123)Random.TaskLocalRNG()
julia> using InteractiveUtils # loaded automatically when working... interactively

Variables scope

The scope of a variable is the region of code where the variable can be accessed directly (without using prefixes). Modules, functions, for and other blocks (but notably not "if" blocks) introduce an inner scope that hinerit from the scope where the block or function is defined (but not, for function, from the caller's scope). Variables that are defined outside any block or function are global for the module where they are defined (the Main module if outside any other module, e.g. on the REPL), the others being local. Variables defined in a for block that already exists as global behave differently depending if we are working interactively or not:

julia> g  = 22
julia> g2 = 2020
julia> for i in 1:2 l1 = g2 # l1: local, g2: global (read only) l1 += i g = i # REPL/INTERACTIVE: global (from Julia 1.5), FILE MODE: local by default (with a warning `g` being already defined) g += i println("i: $i") println("l1: $l1") println("g: $g") for j in 1:2 l1 += j # still the local in outer loop, not a new local one l2 = j g += j println("j: $j") println("l1 inside inner loop: $l1") println("l2 inside inner loop: $l2") println("g inside inner loop: $g") end # println("l2 post: $l2") # error: l2 not defined in this scope println("l1 post: $l1") println("g post: $g") end # println("l1 global $l1") # error; l1 is not defined in the global scopei: 1 l1: 21 g: 2 j: 1 l1 inside inner loop: 22 l2 inside inner loop: 1 g inside inner loop: 3 j: 2 l1 inside inner loop: 24 l2 inside inner loop: 2 g inside inner loop: 5 l1 post: 24 g post: 5 i: 2 l1: 22 g: 4 j: 1 l1 inside inner loop: 23 l2 inside inner loop: 1 g inside inner loop: 5 j: 2 l1 inside inner loop: 25 l2 inside inner loop: 2 g inside inner loop: 7 l1 post: 25 g post: 7
julia> println("g in global: $g") # REPL/INTERACTIVE: "7", FILE MODE: "2"g in global: 7
julia> function foo(i) l1 = g2 # l1: local, g2: global (read only) l1 += i g = i # REPL/INTERACTIVE and FILE MODE: local by default (with a warning `g` being already defined) g += i println("i: $i") println("l1: $l1") println("g: $g") for j in 1:2 l1 += j # still the local in outer loop, not a new local one l2 = j g += j println("j: $j") println("l1 inside inner loop: $l1") println("l2 inside inner loop: $l2") println("g inside inner loop: $g") end # println("l2 post: $l2") # error: l2 not defined in this scope println("l1 post: $l1") println("g post: $g") endfoo (generic function with 1 method)
julia> println("Calling foo..")Calling foo..
julia> foo(10)i: 10 l1: 30 g: 20 j: 1 l1 inside inner loop: 31 l2 inside inner loop: 1 g inside inner loop: 21 j: 2 l1 inside inner loop: 33 l2 inside inner loop: 2 g inside inner loop: 23 l1 post: 33 g post: 23
julia> println("g in global: $g") # REPL/INTERACTIVE: "7", FILE MODE: "2"g in global: 7
julia> g = 22
julia> include("010401-varScopeExample.jl.txt") # gives a warning !┌ Warning: Assignment to `g` in soft scope is ambiguous because a global variable by the same name exists: `g` will be treated as a new local. Disambiguate by using `local g` to suppress this warning or `global g` to assign to the existing global variable. └ @ ~/work/SPMLJ/SPMLJ/buildedDoc/01_-_JULIA1_-_Basic_Julia_programming/010401-varScopeExample.jl.txt:6 i: 1 l1: 21 g: 2 j: 1 l1 inside inner loop: 22 l2 inside inner loop: 1 g inside inner loop: 3 j: 2 l1 inside inner loop: 24 l2 inside inner loop: 2 g inside inner loop: 5 l1 post: 24 g post: 5 i: 2 l1: 22 g: 4 j: 1 l1 inside inner loop: 23 l2 inside inner loop: 1 g inside inner loop: 5 j: 2 l1 inside inner loop: 25 l2 inside inner loop: 2 g inside inner loop: 7 l1 post: 25 g post: 7 g in global: 2 Calling foo.. i: 10 l1: 30 g: 20 j: 1 l1 inside inner loop: 31 l2 inside inner loop: 1 g inside inner loop: 21 j: 2 l1 inside inner loop: 33 l2 inside inner loop: 2 g inside inner loop: 23 l1 post: 33 g post: 23 g in global: 2

Repeated iterations: for and while loops, Array Comprehension, Maps

julia> for i in 1:2, j in 3:4             # j is the inner loop
           println("i: $i, j: $j")
       endi: 1, j: 3
i: 1, j: 4
i: 2, j: 3
i: 2, j: 4
julia> a = 11
julia> while true # or condition, e.g. while a == 10 global a += 1 println("a: $a") if a == 10 break else continue end println("This is never printed") enda: 2 a: 3 a: 4 a: 5 a: 6 a: 7 a: 8 a: 9 a: 10

Array Comprehension

julia> [ i+j for i in 1:2, j in 3:4 if j >= 4]2-element Vector{Int64}:
 5
 6

Maps

Apply a (possible anonymous) function to a list of arguments:

julia> map((name,year) -> println("$name is $year year old"), ["Marc","Anna"], [25,22])Marc is 25 year old
Anna is 22 year old
2-element Vector{Nothing}:
 nothing
 nothing
Warninng

Don't confuse the single-line arrows used in anonymous functions (->) with the double-line arrow used to define a Pair (=>)

We can use maps to substitute values of an array based on a dictionary:

julia> countries = ["US","UK","IT","UK","UK"]5-element Vector{String}:
 "US"
 "UK"
 "IT"
 "UK"
 "UK"
julia> countryNames = Dict("IT" => "Italy", "UK" => "United Kngdom", "US"=>"United States")Dict{String, String} with 3 entries: "IT" => "Italy" "UK" => "United Kngdom" "US" => "United States"
julia> countryLongNames = map(cShortName -> countryNames[cShortName], countries)5-element Vector{String}: "United States" "United Kngdom" "Italy" "United Kngdom" "United Kngdom"

Conditional statements: if blocks and ternary operators

julia> a = 1010
julia> if a < 4 # use `!`, `&&` and `||` for "not", "and" and "or" conditions println("a < 4") elseif a < 8 println("a < 8") else println("a is big!") enda is big!
julia> a = 1010
julia> if a < 5 b = 100 else b = 200 end200
julia> b200

Ternary operators

julia> b =  a < 5 ? 100 : 200   # ? condition : if true : if false200

Short-circuit evaluation

julia> b = 100100
julia> (a < 5) || (b = 200) # replace an if: the second part is executed unless the first part is already true200
julia> b200
julia> (a < 50) || (b = 500) # here is never executedtrue
julia> b200
Warning

Don't confuse boolean operators && and || with their analogous & and | bitwise operators

julia> a = 33
julia> b = 22
julia> bitstring(a)"0000000000000000000000000000000000000000000000000000000000000011"
julia> bitstring(b) ##a && b # error non boolean used in boolean context"0000000000000000000000000000000000000000000000000000000000000010"
julia> a & b2
julia> a | b3

Functions

julia> function foo(x)             # function definition
           x+2
       endfoo (generic function with 1 method)
julia> foo(2) # function call4
julia> inlineFunction(x) = x+2inlineFunction (generic function with 1 method)
julia> foo2 = x -> x+2 # anonymous function (aka "lambda function") and assignment to the variable foo2#9 (generic function with 1 method)
julia> foo2(2)4

A nested function:

julia> function f1(x)
           function f2(x,y)
               x+y
           end
           f2(x,2)
       endf1 (generic function with 1 method)
julia> f1(2)4

A recursive function:

julia> function fib(n)                   # This is a naive implementation. Much faster implementations of the Fibonacci numbers exist
           if n == 0  return 0
           elseif n == 1 return 1
           else
            return fib(n-1) + fib(n-2)
           end
       endfib (generic function with 1 method)
julia> fib(4)3

Function arguments

Positional vs keyword arguments

julia> f(a,b=1;c=1) = a+10b+100c  # `a` and `b` are positional arguments (`b` with a default provided), `c` is a keyword argumentf (generic function with 2 methods)
julia> f(2)112
julia> f(2,c=3)312

We can use the splat operator (...) to specific a variable number of arguments. Here an example for variable positional arguments...

julia> foo(a, args...;c=1) = a + length(args) + sum(args) + cfoo (generic function with 2 methods)
julia> foo(1,2,3,c=4)12

...and here for variable keyword arguments:

julia> fooinner(;a=1,b=10) = a+bfooinner (generic function with 1 method)
julia> function foo(x;y="aaa",kwargs...) println("y is $y") for (k,v) in kwargs println(k," ",v) # note there is no `y` end fooinner(;kwargs...) endfoo (generic function with 2 methods)
julia> foo(10,a=10,b=20,y="bbb")y is bbb a 10 b 20 30

Note that while normally using the semicolon instead of the colon for separating keyword arguments in function calls is optional, when we "forward" variable keyword arguments to a inner function we MUST use the semicolon in the call too. Variable positional arguments can be constrained in the type and size, eventually parametrically, using foo(a,x::Vararg{Float64,2}) = ... (note that then the splat operator is not needed).

Rules for positional and keyword arguments:

  • keyword arguments follow a semicolon ; in the parameters list of the function definition
  • a positional argument without a default can not follow a positional argument with a default provided
  • the splat operator to define variable number of (positional|keyword) arguments must be the last (positional|keyword) argument
  • the function call must use positional arguments by position and keyword arguments by name
julia> # foo0(a::String="aaa",b::Int64) = "$a "+string(b) # error! Optional positional argument before a mandatory positional one
       foo0(b::Int64,a::String="aaa")  = "$a "+string(b) # fine!foo0 (generic function with 2 methods)

Argument types and multiple dispatch

Simple to understand the usage, complex to understand the deep implications

julia> foo3(a::Int64,b::String) = a + parse(Int64,b)foo3 (generic function with 1 method)
julia> foo3(2,"3")5
julia> foo3(a::String,b::Int64) = parse(Int64,a) + bfoo3 (generic function with 2 methods)
julia> foo3("3",2)5
julia> methods(foo3)# 2 methods for generic function "foo3" from Main.var"Main": [1] foo3(a::String, b::Int64) @ REPL[3]:1 [2] foo3(a::Int64, b::String) @ REPL[1]:1

Multiple dispatch allows to compile a specialised version JIT at run time, on the first call with the given parameters type We will see it again when dealing with type inheritance In general, unless we need to write specialised methods, no need to specify the type of the parameters. No influence on performances, this is automatically inferred (and the funciton compiled) based on the run-time type of the argument

!!! tip Functions performances tip The most important things for performances are (1) that the function is type stable, that is, that conditional to a specific combination of the types of the parameters the function returns the same type. This is a condition necessary to have a working chain of type inference across function calls; (2) that no (non constant) global constants are used in the function and indeed all the required information for the functio ndoing its work is embedded in the function parameters

Function templates

julia> foo3(a::T,b::String) where {T<: Number} = a + parse(T,b)             # can use T in the function bodyfoo3 (generic function with 3 methods)
julia> foo3(2,"1")3
julia> foo3(1.5,"1.5")3.0
julia> foo4(a::Int64,b::T where T <: Number) = a + b # ok not used in functio nbodyfoo4 (generic function with 1 method)
julia> foo4(a::Int64,b::Array{T} where T <: Number) = a .+ fill(T,b,2) # wil lerror, can't use T in the function body # foo4(2,[1,2]) # run time error, T not definedfoo4 (generic function with 2 methods)

Call by reference vs. call by value

How the variable used as function argument within the function body relates to the variable used in calling the function ?

  • call by value: the value of the argument is copied and the function body works on a copy of the value
  • call by reference: the function works on the same object being referenced by the caller variable and the function argument
  • call by sharing (Julia): the arguments are just new local variables that bind the same object. The effects of "modifications" on the local variable on the caller's one depends on the mutability property of the object as we saw in the Types and objects segment:
    • immutable objects: we can only have that the argument is rebinded to other objects. No effects on the original caller object
    • mutable objects: if the argument is rebinded to an other object, no effects on the caller object. If the object is modified, the caller object (being the same object) is also modified
julia> x = 1010
julia> foo(y) = (y = 1)foo (generic function with 2 methods)
julia> foo(x)1
julia> x10
julia> foo(x) = x[1] = 10foo (generic function with 2 methods)
julia> x = [1,2]2-element Vector{Int64}: 1 2
julia> foo(x)10
julia> x2-element Vector{Int64}: 10 2
julia> foo3(x) = x = [10,20]foo3 (generic function with 4 methods)
julia> foo3(x)2-element Vector{Int64}: 10 20
julia> x2-element Vector{Int64}: 10 2
Info

Functions that modify at least one of their arguments are named, by convention, with an exclamation mark at the end of their name and the argument(s) that is (are) modified set as the first(s) argument(s)

julia> foo!(x) = x[1] = 10 # to follow the conventionfoo! (generic function with 1 method)

do blocks

Functions that accept an other function as their first parameter can be rewritten with the function itself defined in a do block:

julia> using Statistics
julia> pool(f,x,poolSize=3) = [f(x[i:i+poolSize-1]) for i in 1:length(x)-poolSize+1] # a real case, used in neural networks as pooling layerpool (generic function with 2 methods)
julia> pool(mean,[1,2,3,4,5,6])4-element Vector{Float64}: 2.0 3.0 4.0 5.0
julia> pool(maximum,[1,2,3,4,5,6])4-element Vector{Int64}: 3 4 5 6
julia> pool([1,2,3,4,5]) do x # x is a local variable within the do block. We need as many local variables as the number of parameters of the inner function sum(x)/length(x) end3-element Vector{Float64}: 2.0 3.0 4.0

Using the do block we can call the outer function and define the inner function at the same time. do blocks are frequently used in input/output operations

View this file on Github.


This page was generated using Literate.jl.