Write functions!
Last updated on 2024-11-14 | Edit this page
Estimated time: 20 minutes
Overview
Questions
- “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:
OUTPUT
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.
Challenge
Let’s take a closer look at the signature of the names
function:
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)
-
names(Trebuchets; all = true)
- Answer 2 and 4
- 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.
- 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 also correct: you can specify
where the positional arguments end with the
;
, but you do not have to. - This is the most correct answer.
Before starting to work in a new document, Melissa has to:
Activate her environment
Activating project at `~/projects/trebuchet`
Importing the package under its modified name
Defining the structures
JULIA
mutable struct Trebuchet <: AbstractVector{Float64}
counterweight::Float64
release_angle::Float64
end
struct Environment
wind::Float64
target_distance::Float64
end
Now she can execute
OUTPUT
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 ?):
OUTPUT
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.
Generic functions and methods
In the output we see that shoot
has two different
argument signatures: one with three arguments and one with a
Tuple
of three elements as its single argument. These two
signatures correspond to two different implementations. In our case one
is calling the other.
Functions of the same name with different argument signatures are
called methods of a generic function of that name. In
our example we have two methods of the shoot
generic
function.
Almost all function in Julia are generic functions and in particular
all user defined functions. An example with particularly many methods is
+
. You can list its methods by executing
methods(+)
, for example.
Julia determines which method to apply to a tuple of arguments according to set of rules, which are documented in the Julia Manual’s Methods section.
Now she is ready to fire the first shot.
OUTPUT
(Trebuchet.TrebuchetState(Trebuchet.Lengths{Float64}(1.524, 2.0702016, 0.5334, 0.6096, 2.0826984, 0.8311896, 0.037947600000000005), Trebuchet.Masses{Float64}(226.796185, 0.14877829736, 4.8307587405), Trebuchet.Angles{Float64}(-0.4328124904398228, 1.1928977546511481, 1.437218009822302), Trebuchet.AnglularVelocities{Float64}(-6.80709816163242, 10.240657933288563, -22.420510883318446), Trebuchet.Constants{Float64}(5.0, 1.0, 1.0, 9.80665, 0.7853981633974482), Trebuchet.Inertias{Float64}(0.042140110093804806, 2.7288719786342384), Val{:End}(), 60.0, Trebuchet.Vec(114.88494815382731, -1.5239999999999991), Trebuchet.Vec(10.886295450427806, -21.290442812748466), Solution(387)
, 3.943408301947865, Val{:Released}()), 114.88494815382731)
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:
OUTPUT
114.88494815382731
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:
JULIA
function shoot_distance(windspeed, angle, weight)
Trebuchets.shoot(windspeed, angle, weight)[2]
end
OUTPUT
shoot_distance (generic function with 1 method)
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:
OUTPUT
114.88494815382731
Adding methods
Since Melissa wants to work with the structs Trebuchet
and Environment
, she adds another convenience method for
those:
JULIA
function shoot_distance(trebuchet::Trebuchet, env::Environment)
shoot_distance(env.wind, trebuchet.release_angle, trebuchet.counterweight)
end
OUTPUT
shoot_distance (generic function with 2 methods)
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:
OUTPUT
shoot_distance (generic function with 3 methods)
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,
OUTPUT
#1 (generic function with 1 method)
or in long form, by omitting the name:
OUTPUT
#3 (generic function with 1 method)
Calling methods
Now, that she defined all these methods she tests calling a few
OUTPUT
114.88494815382731
OUTPUT
114.88494815382731
For the other method she needs to construct Trebuchet
and Environment
objects first
OUTPUT
Environment(5.0, 100.0)
ERROR
MethodError: no method matching size(::Trebuchet)
Closest candidates are:
size(::AbstractArray{T, N}, !Matched::Any) where {T, N}
@ Base abstractarray.jl:42
size(!Matched::Union{LinearAlgebra.QR, LinearAlgebra.QRCompactWY, LinearAlgebra.QRPivoted})
@ LinearAlgebra /opt/hostedtoolcache/julia/1.9.4/x64/share/julia/stdlib/v1.9/LinearAlgebra/src/qr.jl:582
size(!Matched::Union{LinearAlgebra.QR, LinearAlgebra.QRCompactWY, LinearAlgebra.QRPivoted}, !Matched::Integer)
@ LinearAlgebra /opt/hostedtoolcache/julia/1.9.4/x64/share/julia/stdlib/v1.9/LinearAlgebra/src/qr.jl:581
...
Errors and finding documentation
This error tells her two things:
- a function named
size
was called - it didn’t have a method for
Trebuchet
Melissa wants to add the missing method to size
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.
OUTPUT
Base
Now Melissa knows she needs to add a method to Base.size
with the signature (::Trebuchet)
.
She can also lookup the docstring using the @doc
macro
OUTPUT
size(A::AbstractArray, [dim])
Return a tuple containing the dimensions of A. Optionally you can specify a
dimension to just get the length of that dimension.
Note that size may not be defined for arrays with non-standard indices, in
which case axes may be useful. See the manual chapter on arrays with custom
indices.
See also: length, ndims, eachindex, sizeof.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> A = fill(1, (2,3,4));
julia> size(A)
(2, 3, 4)
julia> size(A, 2)
3
size(cb::CircularBuffer)
Return a tuple with the size of the buffer.
size(g, i)
Return the number of vertices in g if i=1 or i=2, or 1 otherwise.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> using Graphs
julia> g = cycle_graph(4);
julia> size(g, 1)
4
julia> size(g, 2)
4
julia> size(g, 3)
1
With that information she can now implement this method:
But that is a 3 lines of text for a very short definition. Melissa can also using the short form notation to fit this in a single line:
!!! callout Omitting unneeded arguments Melissa could also name the
argument in the signature. Like this:
(trebuchet::Trebuchet)
, but since the argument is not
needed to compute the output of the function she can omit it. The
argument is in this case only used to dispatch to the correct method.
Now she can try again
ERROR
CanonicalIndexError: getindex not defined for Trebuchet
Again, there is an error but this time the error message is
different: It’s no longer a method for size
that is missing
but for getindex
. She looks up the documentation for that
function
OUTPUT
getindex(type[, elements...])
Construct a 1-d array of the specified type. This is usually called with the
syntax Type[]. Element values can be specified using Type[a,b,c,...].
Examples
≡≡≡≡≡≡≡≡≡≡
julia> Int8[1, 2, 3]
3-element Vector{Int8}:
1
2
3
julia> getindex(Int8, 1, 2, 3)
3-element Vector{Int8}:
1
2
3
getindex(collection, key...)
Retrieve the value(s) stored at the given key or index within a collection.
The syntax a[i,j,...] is converted by the compiler to getindex(a, i, j,
...).
See also get, keys, eachindex.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> A = Dict("a" => 1, "b" => 2)
Dict{String, Int64} with 2 entries:
"b" => 2
"a" => 1
julia> getindex(A, "a")
1
getindex(A, inds...)
Return a subset of array A as specified by inds, where each ind may be, for
example, an Int, an AbstractRange, or a Vector. See the manual section on
array indexing for details.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> A = [1 2; 3 4]
2×2 Matrix{Int64}:
1 2
3 4
julia> getindex(A, 1)
1
julia> getindex(A, [2, 1])
2-element Vector{Int64}:
3
1
julia> getindex(A, 2:4)
3-element Vector{Int64}:
3
2
4
getindex(tree::GitTree, target::AbstractString) -> GitObject
Look up target path in the tree, returning a GitObject (a GitBlob in the
case of a file, or another GitTree if looking up a directory).
Examples
≡≡≡≡≡≡≡≡≡≡
tree = LibGit2.GitTree(repo, "HEAD^{tree}")
readme = tree["README.md"]
subtree = tree["test"]
runtests = subtree["runtests.jl"]
observable[]
Returns the current value of observable.
getindex(A::ArrayPartition, i::Colon, j...)
Returns the entry at index j... of every partition of A.
getindex(A::ArrayPartition, ::Colon)
Returns a vector with all elements of array partition A.
v = sd[k]
Argument sd is a SortedDict and k is a key. In an expression, this retrieves
the value (v) associated with the key (or KeyError if none). On the
left-hand side of an assignment, this assigns or reassigns the value
associated with the key. (For assigning and reassigning, see also insert!
below.) Time: O(c log n)
cb[i]
Get the i-th element of CircularBuffer.
• cb[1] to get the element at the front
• cb[end] to get the element at the back
getindex(tree, ind)
Gets the key present at index ind of the tree. Indexing is done in
increasing order of key.
g[iter]
Return the subgraph induced by iter. Equivalent to induced_subgraph(g,
iter)[1].
Note that the documentation for all methods gets shown and Melissa needs to look for the relevant method first. In this case its the paragraph starting with
getindex(A, inds...)
After a bit of pondering the figures it should be enough to add a
method for getindex
with a single number.
getindex(trebuchet::Trebuchet, i::Int)
Syntactic sugar
In Julia a[1]
is equivalent to
getindex(a, 1)
and a[2] = 3
to
setindex!(a, 3, 2)
Likewise a.b
is equivalent
to getproperty(a, :b)
and a.b = 4
to
setproperty!(a, :b, 4)
.
Key Points
- “You can think of functions being a collection of methods”
- “Methods are defined by their signature”
- “The signature is defined by the number of arguments, their order and their type”
- “Keep the number of positional arguments low”
- “Macros transform Julia expressions”