Skip to main content

Chapter 4 Introduction to Functions

In any language, the function is one of the most important ideas and this chapter covers the introduction of functions with examples in Julia. There are four main purposes of functions in scientific computation.
  1. A function in a computer language mimics that of a mathematical function, which are crucial in this field.
  2. Functions simplify code. Wherever code is repeated either exactly or nearly exactly, functions are one way to reduce the amount of code written and makes code conceptually easier.
  3. Functions abstract code. If you can create a piece of code that does a particular task, then this helps in the abstraction process.
  4. Functions allow separation of code. If there is a large code and a section of it does a specific task, making it a function will separate code resulting in simplification.

Section 4.1 Simple example

A simple example of a Julia function is
function sq(x)
  x*x
end
which just returns the square of the argument, called x. The name of the function is sq and the x inside the parentheses is called the argument of the function. Both the function name and any argument must adhere to the rules of variables from Chapter 3.
An alternative way to write this is the following:
sq(x)=x*x
which is often used if a function is a single line of code. This also looks a lot like a mathematical function. To make it even more mathematical, we could also write this as
sq(x)=x^2
To call a function, it is much like that of any other language. If we type sq(3), which returns 9, the square of 3 and sq(-4) returns 16. In the latter case, this assigns the argument x the value -4 and runs the code in the function.

Section 4.2 Function Arguments

The function arguments are names associated with data passed into the function. In the function sq above, the number x is the only argument. We can have more arguments by separating by commas. The (quite unnecessary function) theSum will take two arguments and add the result:
theSum(x,y) = x+y
Typically, if a function can be written in one line, we will use this style of functions. For more complex functions, use the function keyword as a block of code.

Check Your Understanding 4.1.

Write a function theMean that finds the mean (average) of two arguments x and y.

Section 4.3 Returning values from a function

The functions sq or theSum returned the value that is the last line of the function. If you want to return a value before the last line you can use the return command. The following will return true if the number is odd and false if the number is true:
function isOdd(n)
  if mod(n,2)==1
    return true
  end
  false
end
The mod function is the remainder of n divided by 2 and can also be written n % 2. If the remainder is 1, then the number is odd. On line #3, return true the code stops here and exits the function and doesn’t execute any of the other lines.
Note: the == tests for equality. This will be discussed in Chapter 5.
A better way to write this (but doesn’t use the return statement) just evaluates if mod(n,2) is 1 or not:
isOdd(n) = mod(n,2) == 1
which will return true if mod(n,2) is actually 1 and false if it is anything else (but the only other possibility is 0). Again, since this is just one line, we’ll use the shorthand notation.

Subsection 4.3.1 Indentation in Functions

You should notice that the isOdd function written above as a block of 5 lines has different indentation. In Julia, the indentation doesn’t matter
 1 
This isn’t true of all languages. Python, for example, relies significantly on indentation for blocks of code and therefore the end isn’t needed to terminate a block.
but it is standard to indent for clarity. Notice first that in all of the functions so far, the code has been indented
 2 
I have a preference for 2 spaces, but 3 and 4 are common as well.
two spaces and then the if block is indented again 2 spaces. One can use any number of spaces for indentation as long as it is consistent.

Section 4.4 Specifying Argument Types

The isOdd function above should only work on integers, but if type isOdd(3.5) you get a result. (But does it give you want you want?). What if you enter isOdd("odd")? Try it.
Since odd numbers only make sense with positive integers, declaring the type of argument makes sense. Therefore, if instead, you specify a type in the following way:
isOdd(n::Integer) = mod(n,2)==1
where the double colon, :: tells the type of the argument n. Any type that is an Integer can go in here and note that we used the abstract data type Integer as seen in Section 21.3.
Also, make sure to restart the kernel and details on how to do this can be found in Section A.7. After restarting the kernel make sure that you rerun the function isOdd above.
Once you have done this, entering isOdd(3.5) returns
MethodError: no method matching isOdd(::Float64)
The function `isOdd` exists, but no method is defined for this combination of argument types. 
This just means that there is not function isOdd that takes a Float64 as an argument, which is what we want. A similar error should now occur for isOdd("odd").
Throughout the rest of this text, we will always specific an argument type. This is not only good style and practice, we will see that this results in faster code.

Check Your Understanding 4.2.

Rewrite the theMean function from above using types for the arguments. Since both floats and integers are real numbers, use the abstract Real type for this. Restart the kernel and test your function using both numbers (floats, integers or rationals) and non numbers (like a string).
Solution.
theMean(x::Real, y::Real) = (x+y)/2

Section 4.5 Multiple Dispatch

Before starting this, make sure that you have completed the exercise above to write a 2-argument version of the mean and it is currently in the kernel (this means, just run it again, if in doubt.)
It would be nice to have a mean function that takes more than 2 numbers as well, so the following is a three-argument version of the mean function can be written
theMean(x::Real,y::Real,z::Real) = (x+y+z)/3
Depending on the number of arguments, Julia will call the appropriate function. This is an example of Multiple Dispatch, in which either the number or type of arguments determine the actual function call.
If you look back at your document when you declared the functions, you should see that the second one entered said theMean (generic function with 2 methods), which says that there are two functions called mean. Typing methods(theMean) results in:
# 3 methods for generic function theMean from Main:
	theMean(x::Real, y::Real) in Main at /Users/XXXXX/code/sci-comp-book/Julia-output/intro-functions.ipynb:1
	theMean(x, y) in Main at /Users/XXXXX/code/sci-comp-book/Julia-output/intro-functions.ipynb:1
	theMean(x::Real, y::Real, z::Real) in Main at /Users/XXXXX/code/sci-comp-book/Julia-output/intro-functions.ipynb:1
Much of yours will be different depending on using the REPL or a notebook, however the important part is that you will see that there are 3 methods and the rest shows where they were defined.
Multiple dispatch also allows different types of arguments as well. Let’s say we want to create a function theMean that take a single string as a argument like:
function theMean(str::String)
  "This should return the definition of $str"
end
and entering methods(theMean) shows you that there are now 4 functions. Try typing
theMean("definition")

Subsection 4.5.1 Multiple Dispatch of built-in functions

Multiple dispatch allows Julia to be quite nice. For example, the + function allows code to be written with the plus symbol and execute different code. Type methods(+) and the top of the results should be similar to
# 197 methods for generic function + from Base:
+(B::BitMatrix, J::LinearAlgebra.UniformScaling) in LinearAlgebra at /Users/XXXXX/.Julia/Juliaup/Julia-1.11.0-beta2+0.aarch64.apple.darwin14/share/Julia/stdlib/v1.11/LinearAlgebra/src/uniformscaling.jl:151
+(x::Bool, z::Complex{Bool}) in Base at complex.jl:308
+(x::Bool, y::Bool) in Base at bool.jl:166
+(x::Bool) in Base at bool.jl:163
+(x::Bool, z::Complex) in Base at complex.jl:315
+(x::Real, z::Complex{Bool}) in Base at complex.jl:322
+(x::Bool, y::T) where T>:AbstractFloat in Base at bool.jl:173
+(x::Dates.CompoundPeriod, y::Dates.CompoundPeriod) in Dates at /Users/XXXXX/.Julia/Juliaup/Julia-1.11.0-beta2+0.aarch64.apple.darwin14/share/Julia/stdlib/v1.11/Dates/src/periods.jl:335
+(x::Dates.CompoundPeriod, y::Dates.Period) in Dates at /Users/XXXXX/.Julia/Juliaup/Julia-1.11.0-beta2+0.aarch64.apple.darwin14/share/Julia/stdlib/v1.11/Dates/src/periods.jl:333
+(x::Dates.CompoundPeriod, y::Dates.TimeType) in Dates at /Users/XXXXX/.Julia/Juliaup/Julia-1.11.0-beta2+0.aarch64.apple.darwin14/share/Julia/stdlib/v1.11/Dates/src/periods.jl:363
+(dt::Dates.DateTime, y::Dates.Year) in Dates at /Users/XXXXX/.Julia/Juliaup/Julia-1.11.0-beta2+0.aarch64.apple.darwin14/share/Julia/stdlib/v1.11/Dates/src/arithmetic.jl:25
+(dt::Dates.DateTime, z::Dates.Month) in Dates at /Users/XXXXX/.Julia/Juliaup/Julia-1.11.0-beta2+0.aarch64.apple.darwin14/share/Julia/stdlib/v1.11/Dates/src/arithmetic.jl:49
+(x::Dates.DateTime, y::Dates.Quarter) in Dates at /Users/XXXXX/.Julia/Juliaup/Julia-1.11.0-beta2+0.aarch64.apple.darwin14/share/Julia/stdlib/v1.11/Dates/src/arithmetic.jl:77
+(x::Dates.DateTime, y::Dates.Period) in Dates at /Users/XXXXX/.Julia/Juliaup/Julia-1.11.0-beta2+0.aarch64.apple.darwin14/share/Julia/stdlib/v1.11/Dates/src/arithmetic.jl:83
+(z::Complex{Bool}, x::Bool) in Base at complex.jl:309
+(z::Complex{Bool}, x::Real) in Base at complex.jl:323
A few things about this:
  • There are 197 different methods for +. This doesn’t include all of the packages that could be loaded.
  • Each of the methods shows where the method is defined. Generally, you can click on the link and go directly to the code.
  • Line 13 adds a Date and a Month, useful for handles dates.
  • Many of the rest are adding a number (often a Complex) and a Bool.

Section 4.6 Variable Number of arguments

From the last exercise, it would be unfortunate if we have to write different functions for different number of arguments. We can write a variable number of arguments with a ... trailing the last argument. The following is a generalized version of the mean:
function theMean(x::Real...)
  local sum=0
  for val in x
    sum += val
  end
  sum/length(x)
end
which uses a for loop that we will discuss later. This function will now find the mean using any number of arguments. Try theMean(1,2,3,4), theMean(11//2,5//6,1//9) and theMean(1.0,2.0,3.0,4.0,5.0) and determine if it is returning what you expect.
 3 
You may notice that the mean of the two rational numbers results in a floating-point number. A better way to do this would be to return a rational.
Also, note that the argument x with the ... is a tuple, (see Section 6.14). An alternative way to access the individual elements of x would be to use brackets. For example, x[3] would be the third argument.

Section 4.7 Multiple Return Values

A very nice feature of Julia functions is that of multiple return values. Instead of only being able to return a single number (or requiring to send an array or structural type), you can return more than 1 number (or other data type). For example:
function h(x,y)
  x+y,x-y
end
and if you call this, say h(3,5) you will get the result (8,-2) or if you say
p,q=h(3,5)
The variable p will take on the value 8 and q will take on the value of -2.
The result of this function is a tuple as we saw in Section 6.14. Although in that section we used parentheses around the tuple, it is not necessary and generally isn’t used to return a tuple in a function. We will use this for the result of the quadratic formula in Chapter 10.

Section 4.8 Factorial Function

Mathematically, we define the factorial as a function on a non-negative integer as
\begin{equation*} n! = n(n-1)(n-2)\cdots 3 \cdot 2 \cdot 1 \end{equation*}
or the product of all of the numbers from itself down to 1. There are a number of ways to program this function as we will see. Using
function fact(n::Integer)
  local prod=1
  for i=1:n
    prod *= i
  end
  prod
end
uses a for loop and we will see the details of this in Chapter 5. The for loop first assigns i the value 1 then executes the lines, then sets the value to 2, then executes the block, and so on until i is n. Since prod starts as 1, this multiplies prod by every integer between 1 and \(n\text{,}\) and thus is the factorial. The result in prod is returned.

Check Your Understanding 4.3.

(a)
Test the function above for various positive integers.
(b)
What happens if you put in 0 or a negative integer?
(c)
What happens if you put in a number that is not an integer?

Section 4.9 Recursive Functions

Any function that calls itself within its block of code is called a recursive function. One of the standard examples of this is the factorial function.
Above, we saw how to compute the factorial of a number using a for loop. There’s another way to do this. We can define the factorial in the following way:
\begin{equation*} n!= \begin{cases} 1 & n=0 \\ n\cdot(n-1)! & \text{otherwise} \end{cases} \end{equation*}
and this is a mathematical piecewise function that returns 1 if \(n=0\) and otherwise returns \(n\cdot(n-1)!\text{.}\)
The reason that this is recursive is that the factorial function is within the function. That is it calls itself.
We can write the Julia version of the factorial in the following:
function factr(n::Integer)
  if n == 0
    return 1
  else
    return n*factr(n-1)
  end
end
where n == 0 tests if the variable n is 0 or not. If n is 0, then 1 is returned. Otherwise, n*factr(n-1) is calculated and then returned. The equality test as well as the if-then-else statement will be covered in detail in Chapter 5.

Check Your Understanding 4.4.

Another example of a recursive function is that of a Fibonacci number. If we let \(f(1)=1\text{,}\) \(f(2)=1\text{,}\) and then
\begin{equation*} f(n)=f(n-1)+f(n-2) \qquad \text{for $n > 2$}. \end{equation*}
The first few values are 1, 1, 2, 3, 5, 8, 13, 21, ... Write a recursive function that produces fibonacci numbers. Test it on values of \(n\) that are smaller than 20
 4 
If you play with this a bit, you will that computing the fibonacci numbers in a recursive manner, although simple, is very slow as the values increase. In Section 8.8, we will examine another way to compute this more quickly.

Section 4.10 Summary of Function basics

Functions are useful to separate code into a series of statements that should do one thing. A function is designated with a name and has arguments which should be typed for clarity and speed.
Julia documentation on functions is a has additional information on functions and we will cover some advanced features of functions in Chapter 7 and Chapter 11.