3.4 Functions

Functions are doers, i.e encapsulated pieces of code that do things for us. Optimally, a function should be single minded, i.e. doing one thing only and doing it well. Moreover since they do stuff their names should contain verbs (whereas variables’ names should be composed of nouns).

We already met one of many Julia’s built in functions, namely println (see Section 2.2). As the name suggests it prints something (like a text) to the screen (more precisely standard output).

3.4.1 Mathematical functions

We can also define some functions on our own:

function getRectangleArea(lenSideA::Real, lenSideB::Real)::Real
    return lenSideA * lenSideB
end
getRectangleArea (generic function with 1 method)

Here I declared Julia’s version of a mathematical function. It is called getRectangleArea and it calculates (surprise, surprise) the area of a rectangle.

To do that I used the keyword function. The function keyword is followed by the name of the function (getRectangleArea). Inside the parenthesis are arguments of the function. The function accepts two arguments lenSideA (length of one side) and lenSideB (length of the other side) and calculates the area of a rectangle (by multiplying lenSideA by lenSideB). Both lenSideA and lenSideB are of type Real. It is Julia’s representation of a real number, it encompasses (it’s kind of a supertype), among others, Int and Float64 that we encountered before. The ending of the first line, )::Real, signifies that the function will return a value of type Real. The stuff that function returns is preceded by the return keyword. The function ends with the end keyword.

Note: A Julia’s function does not need the return keyword since it returns the result of its last expression. Still, I prefer to be explicit.

Time to run our function and see how it works.

getRectangleArea(3, 4)

12

getRectangleArea(1.5, 2)

3.0

Note: In some other languages, e.g. Python, you could use the function like: getRectangleArea(3, 4), getRectangleArea(lenSideA=3, lenSideB=4) or getRectangleArea(lenSideB=4, lenSideA=3). However, for performance reasons (and perhaps due to its Lisp heritage) Julia’s functions accept arguments in a positional manner (although Julia has keyword arguments). Therefore, here you may only use getRectangleArea(3, 4) form. Internally, the first argument (3) will be assigned to the local variable lenSideA and the second (4) to the local variable lenSideB inside the getRectangleArea function.

Hmm, OK, I got getRectangleArea and what if I need to calculate the area of a square. You got it.

function getSquareArea(lenSideA::Real)::Real
    return getRectangleArea(lenSideA, lenSideA)
end
getSquareArea (generic function with 1 method)

Note: The argument (lenSideA) of getSquareArea is only known inside the function. Another function can use the same name for its arguments and it will not collide with this one. For instance, getRectangleArea(lenSideA::Real, lenSideB::Real) will receive the same number twice, which getSquareArea knows as lenSideA, but getRectangleArea will see only the numbers (it will receive their copies) and it will name them lenSideA and lenSideB for its own usage.

Here I can either write its body from scratch (return lendSideA * lenSideA) or reuse (as I did) our previously defined getRectangleArea. Lesson to be learned here, functions can use other functions. This is especially handy if those inner functions are long and complicated. Anyway, let’s see how it works.

getSquareArea(3)

9

Appears to be working just fine.

A quick reference to the topic we discussed in Section 3.3.1. Here typing getRectangleArea("three", "three") will produce an error. Now, I can spot it right away, read the error’s message and based on that correct my code so the result is in line with my expectations

3.4.2 Functions with generics

Now, let’s say I want a function getFirstElt that accepts a vector and returns its first element (vectors and indexing were briefly discussed in Section 3.3.5).

# works fine for non-empty vectors
function getFirstElt(vect::Vector{Int})::Int
    return vect[1]
end

It looks OK (test it, e.g. getFirstElt([1, 2, 3]). However, the problem is that it works only with integers (or maybe not, test it out). How to make it work with any type, like getFirstElt(["Eve", "Tom", "Alex"]) or getFirstElt([1.1, 2.2, 3.3])?

One way is to declare separate versions of the function for different types of inputs, i.e.

function getFirstElt(vect::Vector{Int})::Int
    return vect[1]
end

function getFirstElt(vect::Vector{Float64})::Float64
    return vect[1]
end

function getFirstElt(vect::Vector{String})::String
    return vect[1]
end
getFirstElt (generic function with 3 methods)

Note: The function’s name is exactly the same in each case. Julia will choose the correct version (aka method, see the output of the code snippet above) based on the type of the argument (vect) send to the function, e.g. getFirstElt([1, 2, 3]), getFirstElt([1.1, 2, 3.0]), and getFirstElt(["a", "b", "c"]) for the three versions above, respectively.

But that is too much typing (I retyped a few times virtually the same code). The other way is to use no type declarations.

function getFirstEltVer2(vect)
    return vect[1]
end

It turns out that you don’t have to declare function types in Julia (just like in the case of variables, see Section 3.3.1) and a function may work just fine.

Still, a die hard ‘typist’ (if I may call a person this way) would probably use so called generic types, like

function getFirstEltVer3(vect::Vector{T})::T where T
    return vect[1]
end

Here we said that the vector is composed of elements of type T (Vector{T}) and that the function will return type T (see )::T). By typing where T we let Julia know that T is our custom type that we just made up and it can be any Julia’s built in type whatsoever (but what it is exactly will be determined once the function is used). We needed to say where T otherwise Julia would throw an error (since it wouldn’t be able to find its own built in type T). Anyway, we could replace T with any other letter (or e.g. two letters) of the alphabet (A, D, or whatever) and the code would still work.

One last remark, it is customary to write generic types with a single capital letter. Notice that in comparison to the function with no type declarations (getFirstEltVer2) the version with generics (getFirstEltVer3) is more informative. You know that the function accepts a vector of some elements, and you know that it returns a value of the same type as the elements that build that vector.

Of course, that last function we wrote for fun (it was fun for me, how about you?). In reality Julia already got a function with a similar functionality (see Base.first).

Note: Functions from Base package, like Base.first mentioned above may be used in a shorter form (without the prefix) like this: first([1, 2, 3, 4]).

Anyway, as I wrote before if you don’t want to use types then don’t, Julia gives you a choice. When I begun to write my first computer programs, I preferred to use programming languages that didn’t require types. However, nowadays I prefer to use them for the reasons similar to those described in Section 3.3.1 so be tolerant and bear with me.

3.4.3 Functions operating on structs

Functions may also work on custom types like the ones created with struct. Do you still remember our Fraction type from Section 3.3.8? I hope so.

Let’s say I want to define a function that adds two fractions. I can proceed like so

function add(f1::Fraction, f2::Fraction)::Fraction
    newDenom::Int = f1.denominator * f2.denominator
    f1NewNom::Int = newDenom / f1.denominator * f1.numerator
    f2NewNom::Int = newDenom / f2.denominator * f2.numerator
    newNom::Int = f1NewNom + f2NewNom
    return Fraction(newNom, newDenom)
end

add(Fraction(1, 3), Fraction(2, 6))
Fraction(12, 18)

Note: The variables newDenom, f1NewNom, f2NewNom, newNom are local, e.g. they are created and exist only inside the function when it is called (like here with add(Fraction(1, 3), Fraction(2, 6))) and do not affect the variables outside the function even if they happened to have the same names.

Works correctly, but the addition algorithm is not optimal (for now you don’t have to worry too much about the function’s hairy internals). Luckily the built in Rational type (Section 3.3.8) is more polished. Observe

# equivalent to: Rational(1, 3) + Rational(2, 6)
1//3 + 2//6

2//3

Much better (\(\frac{12}{18} = \frac{12 / 6}{18 / 6} = \frac{2}{3}\)). Of course also other operations like subtraction, multiplication and division work for Rational.

We will meet some functions operating on structs when we use custom made libraries (e.g. Htests.pvalue that works on the object (struct) returned by Htests.OneWayANOVATest in the upcoming Section 5.5). Again, for now don’t worry about it too much.

3.4.4 Functions modifying arguments

Previously (see Section 3.3.5) we said that we can change elements of a vector. Sometimes even unintentionally, because, e.g. we may forget that Arrayss/Vectors are assigned/passed by references (as mentioned in Section 3.3.7).

function wrongReplaceFirstElt(
    ints::Vector{Int}, newElt::Int)::Vector{Int}
    ints[1] = newElt
    return ints
end

xx = [2, 2]
yy = wrongReplaceFirstElt(xx, 3)

# unintentionally we changed xx defined outside a function
(xx, yy)
([3, 2], [3, 2])

Let’s try to re-write the function that changes the first element improving upon it at the same time.

# the function works fine for non-empty vectors
function replaceFirstElt!(vect::Vector{T}, newElt::T) where T
    vect[1] = newElt
    return nothing
end

Note: The function’s name ends with ! (exclamation mark). This is one of the Julia’s conventions to mark a function that modifies its arguments.

In general, you should try to write a function that does not modify its arguments (as modification often causes errors, especially in big programs). However, such modifications are sometimes useful, therefore Julia allows you to do so, but you should always be explicit about it. That is why it is customary to end the name of such a function with ! (exclamation mark draws attention).

Additionally, observe that T can be of any type, but we require newElt to be of the same type as the elements in vect. Moreover, since we modify the arguments we wrote return nothing (to be explicit we do not return a thing) and removed returned type after the function’s name, i.e. we used [) where T instead of )::Vector{T} where T].

Let’s see how the function works.

x = [1, 2, 3]
y = replaceFirstElt!(x, 4)
(x, y)
([4, 2, 3], nothing)

Let me finish this subsection by mentioning a classical example of a built-in function that modifies its argument. The function is push!. It adds elements to a collection (e.g. Arrays, or Vectors). Observe:

xx = [] # empty vector
push!(xx, 1, 2) # now xx is [1, 2]
push!(xx, 3) # now xx is [1, 2, 3]
push!(xx, 4, 5) # now xx is [1, 2, 3, 4, 5]

I mentioned it since that was my favorite way of constructing a vector (to start with an empty vector and add elements one by one with a for loop that we will meet in Section 3.6.1) back in the day when I started my programming journey. Nowadays I do it a bit differently, but I thought it would be good to mention it in case you find it useful while solving some exercises from this book.

3.4.5 Side Effects vs Returned Values

Notice that so far we encountered two types of Julia’s functions:

The difference between the two may not be clear while we use the interactive mode. To make it more obvious let’s put them in the script like so:

# file: sideEffsVsReturnVals.jl

# you should define a function before you call it
function getRectangleArea(lenSideA::Number, lenSideB::Number)::Number
    return lenSideA * lenSideB
end

println("Hello World!")

getRectangleArea(3, 2) # calling the function

After running the code from terminal:

cd folder_with_the_sideEffsVsReturnVals.jl
julia sideEffsVsReturnVals.jl

I got printed on the screen:

Hello World!

That’s it. I got only one line of output, the rectangle area seems to be missing. We must remember that a computer does only what we tell it to do, nothing more, nothing less. Here we said:

In the second case the result went into the void (“If a tree falls in a forest and no one is around to hear it, does it make a sound?”).

If we want to print both pieces of information on the screen we should modify our script to look like:

# file: sideEffsVsReturnVals.jl

# you should define a function before you call it
function getRectangleArea(lenSideA::Number, lenSideB::Number)::Number
    return lenSideA * lenSideB
end

println("Hello World!")

# println takes 0 or more arguments (separated by commas)
# if necessary arguments are converted to strings and printed
println("Rectangle area = ", getRectangleArea(3, 2), "[cm^2]")

Now when we run julia sideEffsVsReturnVals.jl from terminal, we get:

Hello World!
Rectangle area = 6 [cm^2]

More information about functions can be found, e.g. in this section of Julia’s Docs.

If You ever encounter a built in function that you don’t know, you may always search for it in the docs (search box: top left corner of the page).



CC BY-NC-SA 4.0 Bartlomiej Lukaszuk