Functions allow you to package your code for easy reuse, and this post shows the basics of writing our own functions as well as calling them. Functions are pivotal to the Julia language, and the topic is too big to fit into three blog posts, let alone into this one.
In some elementary math textbooks, functions are pictured as a machine with a hopper at the top into which one inserts numbers, and an exit port where the answer comes out. Functions in Julia are similar to regular mathematical functions: they transform input values into one (or more) output values. This intuition carries over to programming languages.
Customary Way of Defining Functions¶
Julia supports multiple ways for defining functions. The way that is most similar to other languages is this:
xxxxxxxxxx
function addTwoNumbers(x, y)
x + y
end
addTwoNumbers (generic function with 1 method)
To use this function (to "call" it), just pass-in some values:
xxxxxxxxxx
addTwoNumbers(3, 5)
8
In this example, "addTwoNumbers
" is the name of the function, and the two values in parentheses are called arguments. The statement(s) after the name and arguments but before the end is called the body of the function.
The valid naming of functions follows the same rules as naming variables - no reserved words, etc. There is a convention, however, that function names ending with an exclamation point (like the built-in functions push!() and pop!()) behave in a certain way. We'll see what this means in the "Arrays" chapter. For now, name your functions anyway you want, just no exclamation marks.
In comparison to other languages like C or C++, there are three things to notice:
- The data types of x and y are not specified
- There is no "return" statement indicating what value(s) the function should return
- There is nothing indicating the type of value that the function should return.
Type specifications are optional in Julia; we'll see how to do this in a later post.
As it goes, the last calculation performed is what gets returned. There are times when a "return" would be helpful, and Julia indeed has a "return" statement.
So, this function can be rewritten as:
xxxxxxxxxx
function addTwoNumbers(x, y)
return x + y
end
addTwoNumbers (generic function with 1 method)
and this behaves exactly the same as above.
Function bodies can include more than one line, for example:
xxxxxxxxxx
function findMax(x, y)
if x > y
return x
else
return y
end
end
findMax (generic function with 1 method)
Of course, we can use the trinary operator to write an equivalent function with a much shorter body:
xxxxxxxxxx
function findMax(x, y)
x > y ? x : y
end
findMax (generic function with 1 method)
xxxxxxxxxx
findMax(8, 17)
17
xxxxxxxxxx
findMax(43, 27)
43
Default Values for Arguments¶
Sometimes it makes sense to give default values to a function's arguments. We'll see practical examples later, here's how to define a function with some arguments having default values:
xxxxxxxxxx
function testDefaultValues(x, y = 2, z = 3)
println("x = ", x);
println("y = ", y);
println("z = ", z);
end
testDefaultValues (generic function with 3 methods)
xxxxxxxxxx
testDefaultValues(7, 8, 9)
x = 7 y = 8 z = 9
xxxxxxxxxx
testDefaultValues(7, 8)
x = 7 y = 8 z = 3
xxxxxxxxxx
testDefaultValues(7)
x = 7 y = 2 z = 3
xxxxxxxxxx
# This will give an error since the first argument to testDefaultValues doesn't have a default value!
testDefaultValues()
MethodError: no method matching testDefaultValues() Closest candidates are: testDefaultValues(::Any) at In[8]:1 testDefaultValues(::Any, ::Any) at In[8]:1 testDefaultValues(::Any, ::Any, ::Any) at In[8]:1 Stacktrace: [1] top-level scope @ In[12]:3 [2] eval @ ./boot.jl:360 [inlined] [3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String) @ Base ./loading.jl:1094
So y and z are optional arguments, and when omitted, the default values are used. However, x is required, and when it is omitted, there is an error!
One-Line Function Definitions¶
Functions can be enormously more complicated than the examples thus far! But single line functions are sufficiently common as to warrant a short-hand way of defining them. The addTwoNumbers function can be rewritten in this notation as:
xxxxxxxxxx
addTwoNumbers(x, y) = x + y
addTwoNumbers (generic function with 1 method)
Just about every formula used for calculating areas and volumes, performing unit conversions, and etc., can be written as a one-line function. For example:
xxxxxxxxxx
areaOfCircle(r) = π * r^2
circumferenceOfCircle(r) = 2π * r
volumeOfCylinder(r, h) = π * r^2 * h
surfaceAreaOfCylinder(r, h) = 2π *r * (h + r)
volumeOfCube(s) = s^3
convertFahrenheitToCelsius(tempF) = (tempF - 32) * 5/9
convertMetersToYards(meters) = 1.0936 * meters
convertMetersToYards (generic function with 1 method)
Anonymous Functions¶
Sometimes we will use a function only once and be done with it, so that the function doesn't even need to have a name. These "one off" functions are called anonymous functions, and are written with an -> pointing from the arguments to the result. An anonymous function that behaves just like the addTwoNumbers function can be written as:
(x, y) -> x + y
#1 (generic function with 1 method)
"Wait!" you say, "if this function has no name, then how can it be called? Anonymous functions are supposed to be for single use - how can we even use this function once?"
The answer is, you can't easily do so. Anonymous functions are mostly used as arguments to other functions - yes, functions can be passed into other functions.
We will see practical examples of this in the "Arrays" chapter, which will make all this extremely intuitive. In the meantime, here is a very contrived example:
xxxxxxxxxx
function applyToTwoArguments(f, x, y)
f(x, y)
end
applyToTwoArguments (generic function with 1 method)
So, this function is expecting us to pass-in a function and two additional arguments, and the result will be that passed-in function applied to these two extra agruments.
How to call applyToTwoArguments? There are two possibilities:
- We pass-in a previously defined function for the first argument
- We pass-in an anonymous function
xxxxxxxxxx
# Using a previously defined function:
applyToTwoArguments(addTwoNumbers, 64, 32)
96
xxxxxxxxxx
# Using an anonymous function:
applyToTwoArguments((x, y) -> x + y, 64, 32)
96
Functions that Return Other Functions¶
We just saw that we can use a function (anonymous or otherwise) as an argument to another function. Can functions return other functions? Yes, and here is a practical example.
xxxxxxxxxx
# Use point-slope form of line to get equation in slope-intercept form
# When line is vertical, throw an error
function lineThroughTwoPoints(x1, y1, x2, y2)
if x2 != x1
m = (y2 - y1) / (x2 - x1)
b = y1 - m*x1
return y(x) = m*x + b
else
throw(DivideError())
end
end
lineThroughTwoPoints (generic function with 1 method)
xxxxxxxxxx
# Find equation of line passing through (3, -1) and (6, 17)
# It should be y = 6*x - 19
myLine = lineThroughTwoPoints(3, -1, 6, 17)
(::var"#y#5"{Float64, Float64}) (generic function with 1 method)
Great, but how do we call this new function myLine? Just like any other function with one argument:
xxxxxxxxxx
myLine(0)
-19.0
xxxxxxxxxx
# Of course, there is no slope-intercept form for a vertical line:
verticalLine = lineThroughTwoPoints(1, 0, 1, 5)
DivideError: integer division error Stacktrace: [1] lineThroughTwoPoints(x1::Int64, y1::Int64, x2::Int64, y2::Int64) @ Main ./In[19]:10 [2] top-level scope @ In[22]:3 [3] eval @ ./boot.jl:360 [inlined] [4] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String) @ Base ./loading.jl:1094
Here's another example. In the finite difference calculus, the forward difference of a given function f(x) is f(x + 1) minus f(x), and is usually denoted Δf. So, given a function f(x), the forward difference Δf(x) is a new function. Here's how to implement this (typing \Delta [TAB] to get the capital delta):
function Δ(f)
return d(x) = f(x + 1) - f(x)
end
Δ (generic function with 1 method)
We could, if we wanted to, use this the same way as lineThroughTwoPoints, namely pass in a function into Δ, and store the result in a new function, then apply that new function on a particluar numeric argument, like this:
xxxxxxxxxx
# Example function we'll feed into Δ
f(x) = x^2
forwardDiff = Δ(f)
# Should return (3 + 1)^2 - 3^2 = 16 - 9 = 7
forwardDiff(3)
7
Instead of storing the function returned by Δ into a variable like forwardDiff
then feeding 3 into forwardDiff, we can just perform both actions like this:
# Read the next line as (Δ(f))(3)
Δ(f)(3)
7
Returning Multiple Values¶
Functions can return more than one value, and the Julia assignment statement lets us "unpack" this result.
xxxxxxxxxx
function findMinAndMax(x, y)
if x > y
return y, x
else
return x, y
end
end
findMinAndMax (generic function with 1 method)
xxxxxxxxxx
small, big = findMinAndMax(190, 14)
(14, 190)
xxxxxxxxxx
small
14
xxxxxxxxxx
big
190
Varargs Functions¶
So far, we have seen functions taking a pre-defined number of arguments: addTwoNumbers and findMinAndMax have two arguments, lineThroughTwoPoints takes four arguments, and etc. There are times when we need to write functions that take a variable number of arguments. These functions are usually called varargs functions
.
function addThemUp(nums...)
total = 0
for n in nums
total += n
end
return total
end
addThemUp (generic function with 1 method)
xxxxxxxxxx
addThemUp()
0
xxxxxxxxxx
addThemUp(12)
12
xxxxxxxxxx
addThemUp(14, 5, 8, -5)
22
Let's examine the definition of the addThemUp function
- To indicate that zero or more values can be passed-in, we put ellipsis ("...") after the argument called nums
- Once inside the function, nums is what is called a tuple - a list of values that we can iterate over
- Initialize a variable called "total" to zero
- Loop over the items in the tuple, and add each item to the total
- Finally, return the running total.
When ellipsis is used as part of the argument list in a function definition, this is called "slurping" - when the function is called, all the actual arguments are slurped together and made into a tuple.
Can we have more than one positional argument with ellipsis, something like this: testMultipleTuples(x..., y...)
?
No: how would Julia tell when the first tuple ends and the second begins? How would slurping be performed?
The argument with the ellipsis must be the last argument of the function, and there can only be one of them in an positional argument list.
Sometimes, we want to pass-in some fixed arguments in addition to the variable number of arguments. This is still a varargs function. In order for Julia to differentiate the fixed arguments from the ones that will be slurped into a tuple, the tuple must again be the last argument in a varargs function.
Here's an example that takes a multiplier and a variable number of numbers; it adds those numbers up, the returns the sum times the multiplier.
xxxxxxxxxx
function sumThenMultiply(m, nums...)
m * addThemUp(nums...)
end
sumThenMultiply (generic function with 1 method)
xxxxxxxxxx
sumThenMultiply(5, 1, 2, 3)
30
Let's examine this code very carefully...
- We pass in an argument called m (the multiplier), followed by a variable number of numbers called nums
- Like we saw in addThemUp, once we're inside the function, those values get slurped into a tuple called nums
- Because addThemUp takes a bunch of separate numbers and not a tuple, we cannot pass nums directly to addThemUp
- Instead we need to "splat" the tuple, meaning that we need to convert the tuple into a bunch of separate numbers
- To splat a tuple, just follow it by an ellipsis, hence the line m * addThemUp(nums...)
In summary, the ellipsis has two different meanings depending on how it is used:
- When used in the argument of a function definition, it means to "slurp" the actual arguments into a tuple
- When used when calling a function, it means to "splat" the tuple info individual arguments.
Positional vs Keyword Arguments¶
With some functions that take two arguments, like addTwoNumbers, the order that you pass the values in does not alter the returned value:
xxxxxxxxxx
addTwoNumbers(2, 3) == addTwoNumbers(3, 2)
true
With other functions, like volumeOfCylinder(r, h), the order of the arguments is important
xxxxxxxxxx
volumeOfCylinder(2, 3)
37.69911184307752
xxxxxxxxxx
volumeOfCylinder(3, 2)
56.548667764616276
Arguments like this are called positional arguments - their position in a function's argument's list matters!
What happens when we have a function that has four, five, or more arguments? How do we use that function? Do we have to RTFM? Quelle horreur!
The solution of this problem is called keyword arguments or named arguments. Using kwargs allows us to pass-in the values in any order we want; as a nice bonus, it makes reading the code easier for those of us who can't take 15 minutes out of our hot young lives to RTFM.
Here's an example. Imagine we're doing computer graphics and we're writing a function to draw a circle. What type of information do we need to draw a circle?
- x and y of the circle
- the radius
- the fill color
- the edge color
- the edge thickness
That's six arguments - if we used only positional arguments, we would have to memorize the order that these six arguments are to be passed in. Here's an implementation that uses a mixture of positional and keyword arguments. We'll give two of the keyword args a default value:
xxxxxxxxxx
function drawCircle(x, y, r; fillColor, edgeColor = "black", edgeThickness = 1)
println("x = ", x);
println("y = ", y);
println("r = ", r);
println("fillColor = ", fillColor);
println("edgeColor = ", edgeColor);
println("edgeThickness = ", edgeThickness);
end
drawCircle (generic function with 1 method)
So x, y, and r are positional arguments, and fillColor, edgeColor, edgeThickness are keyword arguments - all the positional arguments are before the semicolon, all keyword arguments come after the semicolon. Let's see how kwargs make our life easier.
xxxxxxxxxx
drawCircle(50, 30, 7, fillColor = "red")
x = 50 y = 30 r = 7 fillColor = red edgeColor = black edgeThickness = 1
xxxxxxxxxx
drawCircle(50, 30, 7, fillColor = "red", edgeColor = "green")
x = 50 y = 30 r = 7 fillColor = red edgeColor = green edgeThickness = 1
xxxxxxxxxx
drawCircle(50, 30, 7, edgeColor = "green", fillColor = "red")
x = 50 y = 30 r = 7 fillColor = red edgeColor = green edgeThickness = 1
Notice that the order of the keyword arguments is irrevelent, what matters is the name of the argument. Notice also that when calling this function, we're using just a comma before fillColor instead of a semicolon. Using a semicolon works, too:
xxxxxxxxxx
drawCircle(50, 30, 7; edgeColor = "green", edgeThickness = 2, fillColor = "red")
x = 50 y = 30 r = 7 fillColor = red edgeColor = green edgeThickness = 2
Julia has a nice bit of "syntatic sugar": if a variable matches the name of a keyword argument, the assignment statement can be omitted. This is the only circumstance when the semicolon must be used. For example:
xxxxxxxxxx
# Variables that conviently match the names of the keyword arguments
edgeColor = "blue"
fillColor = "orange"
"orange"
xxxxxxxxxx
# Without syntatic sugar:
drawCircle(50, 30, 7, edgeColor = edgeColor, fillColor = fillColor)
x = 50 y = 30 r = 7 fillColor = orange edgeColor = blue edgeThickness = 1
xxxxxxxxxx
# With the syntatic sugar (notice the semicolon):
drawCircle(50, 30, 7; edgeColor, fillColor)
x = 50 y = 30 r = 7 fillColor = orange edgeColor = blue edgeThickness = 1
Again, when using this trick, the positional arguments must be separated from the keyword arguments by a semicolon!
Conclusion¶
In this post we saw:
- Three ways of defining functions: conventional definitions, one-line definitions, and anonymous definitions
- That we can assign default values to a function's arguments
- Functions can also return multiple values
- How to create aonymous functions and use them in an (artificial) example
- That a function can be passed-in to another function
- That functions can return other functions
- How to create and use varargs functions
- How to define functions with positional and keyword arguments.
As stated in the introduction to this chapter, the concept of functions is vast in Julia, and we just covered the basics. More information on functions will be given in many subsequent chapters. The good thing, though, is that 100% complete mastery of every nuance of Julia functions is not required in order to define your own functions and to use them.
Problems¶
- Implement the backward difference operator ∇ (typed \nabla [TAB]) and the shift operator E
- Write a function that converts cartesian coordinates (x, y) into polar coordinates (r, θ)
No comments:
Post a Comment