Write functions!
Overview
Teaching: 15 min
Exercises: 5 minQuestions
How do I call a function?
Where can I find help about using a function?
What are methods?
Objectives
usage of positional and keyword arguments
defining named and anonymous functions
reading error messages
Working with functions
Now that Melissa successfully installed the package she wants to figure out what she can do with it.
Julia’s Base
module offers a handy function for inspecting other modules
called names
.
Let’s look at its docstring; remember that pressing ?
opens the help?> prompt:
help?> names
names(x::Module; all::Bool = false, imported::Bool = false)
Get an array of the names exported by a Module, excluding deprecated names.
If all is true, then the list also includes non-exported names defined in
the module, deprecated names, and compiler-generated names. If imported is
true, then names explicitly imported from other modules are also included.
As a special case, all names defined in Main are considered "exported",
since it is not idiomatic to explicitly export names from Main.
In Julia we have two types of arguments: positional and keyword, separated by a semi-colon.
- Positional arguments are determined by their position and thus the order in which arguments are given to the function matters.
- Keyword arguments are passed as a combination of the keyword and the value to the function. They can be given in any order, but they need to have a default value.
Positional and keyword arguments
Let’s take a closer look at the signature of the
names
function:names(x::Module; all::Bool = false, imported::Bool = false)
It takes three arguments:
x
, a positional argument of typeModule
,
followed by a;
all
, a keyword argument of typeBool
with a default value offalse
imported
, anotherBool
keyword argument that defaults tofalse
Suppose Melissa wanted to get all names of the
Trebuchets
module, including those that are not exported. What would the function call look like?
names(Trebuchets, true)
names(Trebuchets, all = true)
names(Trebuchets; all = true)
names(Trebuchets, all)
- Answer 2. and 3.
Solution
- Both arguments are present, but
true
is presented without a keyword. This throws aMethodError: no method matching names(::Module, ::Bool)
- This is a correct call.
- This is also correct: you can specify where the positional arguments end with the
;
, but you do not have to.- Two arguments are present, but the keyword
all
is not assigned a value. This throws aMethodError: no method matching names(::Module, ::typeof(all))
- This is the most correct answer.
Melissa goes ahead and executes
names(Trebuchets)
6-element Vector{Symbol}:
:Trebuchet
:TrebuchetState
:run
:shoot
:simulate
:visualise
which yields the exported names of the Trebuchets
module.
By convention types are named with CamelCase while functions typically have
snake_case.
Since Melissa is interested in simulating shots, she looks at the
shoot
function from Trebuchets
(again, using ?):
help?> Trebuchets.shoot
shoot(ws, angle, w)
shoot((ws, angle, w))
Shoots a Trebuchet with weight w in kg. Releases the weight at the release
angle angle in radians. The current wind speed is ws in m/s.
Returns (t, dist), with travel time t in s and travelled distance dist in m.
Methods
Here we see that the
shoot
function has two different methods. The first one takes three arguments, while the second takes aTuple
with three elements.
Now she is ready to fire the first shot.
Trebuchets.shoot(5, 0.25pi, 500)
(TrebuchetState(Trebuchet.Lengths{Float64}(1.52, 2.07, 0.533, 0.607, 2.08, 0.831, 0.0379),
Trebuchet.Masses{Float64}(226.0, 0.149, 4.83),
Trebuchet.Angles{Float64}(-0.503, 1.32, 1.46),
Trebuchet.AnglularVelocities{Float64}(-5.57, 7.72, -25.4),
Trebuchet.Constants{Float64}(5.0, 1.0, 1.0, 9.81, 0.785),
Trebuchet.Inertias{Float64}(0.042, 2.73),
Val{:End}(),
60.0,
Trebuchet.Vec(117.8, -1.524),
Trebuchet.Vec(10.79, -21.45),
Solution(394),
0,
Val{:Released}()
),
117.8
)
That is a lot of output, but Melissa is actually only interested in the distance, which is the second element of the tuple that was returned. So she tries again and grabs the second element this time:
Trebuchets.shoot(5, 0.25pi, 500)[2]
117.8
which means the shot traveled approximately 118 m.
Defining functions
Melissa wants to make her future work easier and she fears she might forget to take the second element. That’s why she puts it together in a function like this:
function shoot_distance(windspeed, angle, weight)
Trebuchets.shoot(windspeed, angle, weight)[2]
end
Implicit return
Note that Melissa didn’t have to use the
return
keyword, since in Julia the value of the last line will be returned by default. But she could have used an explicit return and the function would behave the same.
Now Melissa can just call her wrapper function:
shoot_distance(5, 0.25pi, 500)
117.8
Adding methods
Since Melissa wants to work with the structs Trebuchet
and Environment
, she
adds another convenience method for those:
function shoot_distance(trebuchet::Trebuchet, env::Environment)
shoot_distance(env.wind, trebuchet.release_angle, trebuchet.counterweight)
end
This method will call the former method and pass the correct fields from the
Trebuchet
and Environment
structures.
Slurping and splatting
By peeking into the documentation, Melissa discovers that she
doesn’t need to explicitly declare all the input arguments.
Instead she can slurp the arguments in the function definition and splat
them in the function body using three dots (...
) like this:
function shoot_distance(args...) # slurping
Trebuchets.shoot(args...)[2] # splatting
end
Anonymous functions
Sometimes it is useful to have a new function and not have to come up with a new name. These are anonymous functions. They can be defined with either the so-called stabby lambda notation,
(windspeed, angle, weight) -> Trebuchets.shoot(windspeed, angle, weight)[2]
or in long form, by omitting the name:
function (windspeed, angle, weight)
Trebuchets.shoot(windspeed, angle, weight)[2]
end
Errors and macros
Melissa would like to set the fields of a Trebuchet
using an index.
She writes
trebuchet[1] = 2
ERROR: MethodError: no method matching setindex!(::Trebuchet, ::Int64, ::Int64)
Stacktrace:
[1] top-level scope
@ REPL[4]:1
The error tells her two things:
- a function named
setindex!
was called - it didn’t have a method for
Trebuchet
Melissa wants to add the missing method to setindex!
but she doesn’t know
where it is defined.
There is a handy macro named @which
that obtains the module where the
function is defined.
Macros
Macro names begin with
@
and they don’t need parentheses or commas to delimit their arguments. Macros can transform any valid Julia expression and are quite powerful. They can be expanded by prepending@macroexpand
to the macro call of interest.
@which setindex!
Base
Now Melissa knows she needs to add a method to Base.setindex!
with the
signature (::Trebuchet, ::Int64, ::Int64)
.
Key Points
You can think of functions being a collection of methods
Keep the number of positional arguments low
Macros transform Julia expressions