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