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).
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)
orgetRectangleArea(lenSideB=4, lenSideA=3)
. However, for performance reasons (and perhaps due to its Lisp heritage) Julia’s functions accept arguments in a positional manner. Therefore, here you may only usegetRectangleArea(3, 4)
form. Internally, the first argument (3
) will be assigned to the local variablelenSideA
and the second (4
) to the local variablelenSideB
inside thegetRectangleArea
function. Keep that in mind since the order of the arguments may sometimes make a difference (e.g. ifgetRectangleArea
relied on division instead of multiplication).
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
) ofgetSquareArea
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, whichgetSquareArea
knows aslenSideA
, butgetRectangleArea
will see only the numbers (it will receive their copies) and it will name themlenSideA
andlenSideB
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
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])
, andgetFirstElt(["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: Usually functions from Base package, like
Base.first
mentioned above, may be used in a shorter form (without the prefix), i.e. 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.
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 withadd(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 struct
s when we use custom made libraries like HypothesisTests
(abbreviated Ht
), e.g. Ht.pvalue
that works on the object (struct) returned by Ht.OneWayANOVATest
(see the upcoming Section 5.5). Again, for now don’t worry about it too much.
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 Arrays
s/Vector
s 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. Array
s, or Vector
s). 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.
Notice that so far we encountered two types of Julia’s functions:
println
)getRectangleArea
)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::Real, lenSideB::Real)::Real
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::Real, lenSideB::Real)::Real
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).