Functions
Contents
Functions¶
A Function by Any Other Name¶
Note
Functions, especially very long ones, can make your script quite difficult to navigate. For this reason, it is a good practice to keep as many funcions as possible in a separate file.
Running code from external files can be done by running include("procedures.jl"),
where procedures.jl
is the name of the file.
You can change your folder by using cd(...)
Functions by function
¶
Functions in Julia
can be defined in several ways.
One and the most typical is simply by using function
.
The structure for using function
is as follows:
function function_name(argument_1, argument_2)
some_operations
output = some_operations
return output
end
An example of defining a very simple functions and its execution:
function add_numbers(x, y)
output = x + y
return output
end
add_numbers (generic function with 1 method)
@show add_numbers(5, 12)
add_numbers(5, 12) = 17
17
@show w = add_numbers(10,10)
w = add_numbers(10, 10) = 20
20
Functions do not need arguments:
function add_first_ten_numbers()
Σ = sum( 1:10 )
return Σ
end
add_first_ten_numbers()
55
Functions can return many outpus:
function add_and_multiply(x, y)
out1 = x+y
out2 = x*y
return out1, out2
end
@show res1, res2 = add_and_multiply(2,3);
(res1, res2) = add_and_multiply(2, 3) = (5, 6)
res1
5
res2
6
“Assignment-form” functions¶
There is a compact alternative for function
: f(args) = some operations
.
Example below:
f(x,y,z) = (x^2 + y)/z
@show f(2,1,3)
f(2, 1, 3) = 1.6666666666666667
1.6666666666666667
Functions in this form can contain multiple lines by using begin
/end
block:
g(x) = begin
y = x^2
y += 1
return(y)
end
g (generic function with 1 method)
Anonymous functions¶
Sometimes we do not need to create named functions.
Anonymous functions allow to create a normal function without names.
Their structure is arg -> operations
with one argument or (arg1, arg2, arg3) -> operations
with many arguments:
x -> x^2
(x,y) -> x+y
#3 (generic function with 1 method)
We can evaluate functions at some values in the following way:
(x -> x^2)(3)
9
(x -> x^2)
is the function and 3
is the argument of this function.
Just like assignment-form functions, anonymous functions can be defined within multiple lines using begin
/end
blocks:
x -> begin
x = x + 1
x = 2*x
return(x)
end
#7 (generic function with 1 method)
map
and anonymous functions¶
Recall our example from here. We can use anonymous functions as the argument of map
function to compute \(\frac{{x_i}^2-1}{3}\) for each element of vector \(\mathbf{x}:\)
x = 1:5;
map(xᵢ -> (xᵢ^2-1)/3, x)
5-element Vector{Float64}:
0.0
1.0
2.6666666666666665
5.0
8.0
Function map(f, c)
transform elements of c
by applying f
(which can be an anonymous but also a named function) to each element.
In most cases this function provides very similar results to broadcast
discussed before.
In my own workflow when I work with anonymous functions I use map
, while I use broadcast
only in the dot (.
) convention.
map
also works with several lists and multi-argument anonymous functions:
map((x,y) -> x^y,
1:4, #array with x elements
[2 2 3 3] #array with y elements
)
4-element Vector{Int64}:
1
4
27
64
Scopes: minor digression¶
Objects defined outside the function, by default, are not available and their values cannot be changed.
σ = 12
function modify_σ()
σ = σ + 1;
return(σ)
end
modify_σ()
UndefVarError: σ not defined
Stacktrace:
[1] modify_σ()
@ Main ./In[15]:4
[2] top-level scope
@ In[15]:8
[3] eval
@ ./boot.jl:360 [inlined]
[4] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
@ Base ./loading.jl:1094
Sometimes, if we have a good reason, we may want to change objects created outside the function and not passed as arguments.
To do it we need to point which objects were created beyond the scope of the function.
This can be done with the use of global
:
σ = 12
function modify_σ()
global σ
σ = σ + 1;
return(σ)
end
modify_σ()
13
Not only does this function access σ
but also changes its value in the global scope:
@show σ
σ = 13
13
It is convenient to use dictionaries as arguments of functions.
Dictionaries passed as the arguments can be unpacked using the macro @unpack
, which we already discussed here.
This way, inside the function we can write a clean code without cluttering our global space.
Supposed that we want to have a function that evaluates the function at \(\mathbb{x} \) with parameters twice as large as the input arguments \((a, b, c)\).
equation = Dict([
("x" , collect(range(1, step=.1, length=100)) ),
("a", 1),
("b", 12),
("c", π)
])
Dict{String, Any} with 4 entries:
"c" => π
"x" => [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 … 10.0, 10.1, 10.2…
"b" => 12
"a" => 1
using Parameters
function multiply_coefficients(X)
@unpack a, b, c = X;
a *= 2;
b *= 2;
c *= 2;
output = deepcopy(X)
#instead of writing:
#output["y"] = @. X["a"]*X["x"]^2 + X["b"]*X["x"] + X["c"]
#we have:
output["y"] = @. a*x^2 + b*x + c
output["a"] = a;
output["b"] = b;
output["c"] = c;
return output
end
multiply_coefficients (generic function with 1 method)
new_result = multiply_coefficients(equation)
Dict{String, Any} with 5 entries:
"c" => 6.28319
"x" => [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 … 10.0, 10.1, 10.2…
"b" => 24
"a" => 2
"y" => 62.2832
The result of executing function is assigned to new_result
.
Inside function multiply_coefficients
, we were referring to a
, b
, c
normally.
However, after function execution unpacked arguments are still unavailable in the global scope:
@show a
UndefVarError: a not defined
Stacktrace:
[1] top-level scope
@ show.jl:955
[2] eval
@ ./boot.jl:360 [inlined]
[3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
@ Base ./loading.jl:1094
Caveat on passing arrays and scalars in functions¶
Consider the following code:
using LinearAlgebra
x = 2
y = [1 2;3 4]
set_to_1(scal, matrix)= begin
scal = 1;
matrix .= 1;
return scal, matrix
end
@show x
@show y
@show set_to_1(x, y)
@show x
@show y;
x = 2
y = [1 2; 3 4]
add_1(x, y) = (1, [1 1; 1 1])
x = 2
y = [1 1; 1 1]
Function set_to_1
did not modify scalar x
but modified array y
.
It is because of how the assignment operator works in Julia
.
Notice that matrix .= 1
has an assignment operator =
, which is applied to an array.
This mean that each element allocated in the memory pointed by matrix
has a new value.
To understand it better recall our earlier discussion on the assignment operator with arrays (and whole dictionaries).
The result of this operation preserves after the execution of the function is finished.
Note
The behavior of the assignment operator inside functions is quite different from other languages. There is some logic behind it but you have to remember it, especially if you are used to other languages as well.
One way to write the function without changing the input array is as follows:
using LinearAlgebra
x = 2
y = [1 2;3 4]
set_to_1(scal, matrix)= begin
scal = 1;
matrix_op = deepcopy(matrix) #new line
matrix_op .= 1; #we use `matrix_op` instead of `matrix`
return scal, matrix_op
end
@show x
@show y
@show set_to_1(x, y)
@show x
@show y;
x = 2
y = [1 2; 3 4]
add_1(x, y) = (1, [1 1; 1 1])
x = 2
y = [1 2; 3 4]
Multi dispatch¶
to be written
function Fun1(x::Float64,y::Float64)
return(x-y)
end
Fun1 (generic function with 2 methods)
function Fun1(x::Array{Float64},y::Array{Float64})
return(x .- y)
end
Fun1 (generic function with 3 methods)
Fun1([1; 2], [3; 2])
2-element Vector{Int64}:
-2
0
Fun1(3,2)
1