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 = 2
2
julia> g2 = 20
20
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 scope
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
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") end
foo (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 = 2
2
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") end
i: 1, j: 3 i: 1, j: 4 i: 2, j: 3 i: 2, j: 4
julia> a = 1
1
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") end
a: 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
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 = 10
10
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!") end
a is big!
julia> a = 10
10
julia> if a < 5 b = 100 else b = 200 end
200
julia> b
200
Ternary operators
julia> b = a < 5 ? 100 : 200 # ? condition : if true : if false
200
Short-circuit evaluation
julia> b = 100
100
julia> (a < 5) || (b = 200) # replace an if: the second part is executed unless the first part is already true
200
julia> b
200
julia> (a < 50) || (b = 500) # here is never executed
true
julia> b
200
Don't confuse boolean operators &&
and ||
with their analogous &
and |
bitwise operators
julia> a = 3
3
julia> b = 2
2
julia> bitstring(a)
"0000000000000000000000000000000000000000000000000000000000000011"
julia> bitstring(b) ##a && b # error non boolean used in boolean context
"0000000000000000000000000000000000000000000000000000000000000010"
julia> a & b
2
julia> a | b
3
Functions
julia> function foo(x) # function definition x+2 end
foo (generic function with 1 method)
julia> foo(2) # function call
4
julia> inlineFunction(x) = x+2
inlineFunction (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) end
f1 (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 end
fib (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 argument
f (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) + c
foo (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+b
fooinner (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...) end
foo (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) + b
foo3 (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 body
foo3 (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 nbody
foo4 (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 defined
foo4 (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 = 10
10
julia> foo(y) = (y = 1)
foo (generic function with 2 methods)
julia> foo(x)
1
julia> x
10
julia> foo(x) = x[1] = 10
foo (generic function with 2 methods)
julia> x = [1,2]
2-element Vector{Int64}: 1 2
julia> foo(x)
10
julia> x
2-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> x
2-element Vector{Int64}: 10 2
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 convention
foo! (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 layer
pool (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) end
3-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
This page was generated using Literate.jl.