Functions
Syntax#
- f(n) = …
- function f(n) … end
- n::Type
- x -> …
- f(n) do … end
Remarks#
Aside from generic functions (which are most common), there are also built-in functions. Such functions include is
, isa
, typeof
, throw
, and similar functions. Built-in functions are typically implemented in C instead of Julia, so they cannot be specialized on argument types for dispatch.
Square a number
This is the easiest syntax to define a function:
square(n) = n * n
To call a function, use round brackets (without spaces in between):
julia> square(10)
100
Functions are objects in Julia, and we can show them in the REPL as with any other objects:
julia> square
square (generic function with 1 method)
All Julia functions are generic (otherwise known as polymorphic) by default. Our square
function works just as well with floating point values:
julia> square(2.5)
6.25
…or even matrices:
julia> square([2 4
2 1])
2×2 Array{Int64,2}:
12 12
6 9
Recursive functions
Simple recursion
Using recursion and the ternary conditional operator, we can create an alternative implementation of the built-in factorial
function:
myfactorial(n) = n == 0 ? 1 : n * myfactorial(n - 1)
Usage:
julia> myfactorial(10)
3628800
Working with trees
Recursive functions are often most useful on data structures, especially tree data structures. Since expressions in Julia are tree structures, recursion can be quite useful for metaprogramming. For instance, the below function gathers a set of all heads used in an expression.
heads(ex::Expr) = reduce(∪, Set((ex.head,)), (heads(a) for a in ex.args))
heads(::Any) = Set{Symbol}()
We can check that our function is working as intended:
julia> heads(:(7 + 4x > 1 > A[0]))
Set(Symbol[:comparison,:ref,:call])
This function is compact and uses a variety of more advanced techniques, such as the reduce
higher order function, the Set
data type, and generator expressions.
Introduction to Dispatch
We can use the ::
syntax to dispatch on the type of an argument.
describe(n::Integer) = "integer $n"
describe(n::AbstractFloat) = "floating point $n"
Usage:
julia> describe(10)
"integer 10"
julia> describe(1.0)
"floating point 1.0"
Unlike many languages, which typically provide either static multiple dispatch or dynamic single dispatch, Julia has full dynamic multiple dispatch. That is, functions can be specialized for more than one argument. This comes in handy when defining specialized methods for operations on certain types, and fallback methods for other types.
describe(n::Integer, m::Integer) = "integers n=$n and m=$m"
describe(n, m::Integer) = "only m=$m is an integer"
describe(n::Integer, m) = "only n=$n is an integer"
Usage:
julia> describe(10, 'x')
"only n=10 is an integer"
julia> describe('x', 10)
"only m=10 is an integer"
julia> describe(10, 10)
"integers n=10 and m=10"
Optional Arguments
Julia allows functions to take optional arguments. Behind the scenes, this is implemented as another special case of multiple dispatch. For instance, let’s solve the popular Fizz Buzz problem. By default, we will do it for numbers in the range 1:10
, but we will allow a different value if necessary. We will also allow different phrases to be used for Fizz
or Buzz
.
function fizzbuzz(xs=1:10, fizz="Fizz", buzz="Buzz")
for i in xs
if i % 15 == 0
println(fizz, buzz)
elseif i % 3 == 0
println(fizz)
elseif i % 5 == 0
println(buzz)
else
println(i)
end
end
end
If we inspect fizzbuzz
in the REPL, it says that there are four methods. One method was created for each combination of arguments allowed.
julia> fizzbuzz
fizzbuzz (generic function with 4 methods)
julia> methods(fizzbuzz)
# 4 methods for generic function "fizzbuzz":
fizzbuzz() at REPL[96]:2
fizzbuzz(xs) at REPL[96]:2
fizzbuzz(xs, fizz) at REPL[96]:2
fizzbuzz(xs, fizz, buzz) at REPL[96]:2
We can verify that our default values are used when no parameters are provided:
julia> fizzbuzz()
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
but that the optional parameters are accepted and respected if we provide them:
julia> fizzbuzz(5:8, "fuzz", "bizz")
bizz
fuzz
7
8
Parametric Dispatch
It is frequently the case that a function should dispatch on parametric types, such as Vector{T}
or Dict{K,V}
, but the type parameters are not fixed. This case can be dealt with by using parametric dispatch:
julia> foo{T<:Number}(xs::Vector{T}) = @show xs .+ 1
foo (generic function with 1 method)
julia> foo(xs::Vector) = @show xs
foo (generic function with 2 methods)
julia> foo([1, 2, 3])
xs .+ 1 = [2,3,4]
3-element Array{Int64,1}:
2
3
4
julia> foo([1.0, 2.0, 3.0])
xs .+ 1 = [2.0,3.0,4.0]
3-element Array{Float64,1}:
2.0
3.0
4.0
julia> foo(["x", "y", "z"])
xs = String["x","y","z"]
3-element Array{String,1}:
"x"
"y"
"z"
One may be tempted to simply write xs::Vector{Number}
. But this only works for objects whose type is explicitly Vector{Number}
:
julia> isa(Number[1, 2], Vector{Number})
true
julia> isa(Int[1, 2], Vector{Number})
false
This is due to parametric invariance: the object Int[1, 2]
is not a Vector{Number}
, because it can only contain Int
s, whereas a Vector{Number}
would be expected to be able to contain any kinds of numbers.
Writing Generic Code
Dispatch is an incredibly powerful feature, but frequently it is better to write generic code that works for all types, instead of specializing code for each type. Writing generic code avoids code duplication.
For example, here is code to compute the sum of squares of a vector of integers:
function sumsq(v::Vector{Int})
s = 0
for x in v
s += x ^ 2
end
s
end
But this code only works for a vector of Int
s. It will not work on a UnitRange
:
julia> sumsq(1:10)
ERROR: MethodError: no method matching sumsq(::UnitRange{Int64})
Closest candidates are:
sumsq(::Array{Int64,1}) at REPL[8]:2
It will not work on a Vector{Float64}
:
julia> sumsq([1.0, 2.0])
ERROR: MethodError: no method matching sumsq(::Array{Float64,1})
Closest candidates are:
sumsq(::Array{Int64,1}) at REPL[8]:2
A better way to write this sumsq
function should be
function sumsq(v::AbstractVector)
s = zero(eltype(v))
for x in v
s += x ^ 2
end
s
end
This will work on the two cases listed above. But there are some collections that we might want to sum the squares of that aren’t vectors at all, in any sense. For instance,
julia> sumsq(take(countfrom(1), 100))
ERROR: MethodError: no method matching sumsq(::Base.Take{Base.Count{Int64}})
Closest candidates are:
sumsq(::Array{Int64,1}) at REPL[8]:2
sumsq(::AbstractArray{T,1}) at REPL[11]:2
shows that we cannot sum the squares of a lazy iterable.
An even more generic implementation is simply
function sumsq(v)
s = zero(eltype(v))
for x in v
s += x ^ 2
end
s
end
Which works in all cases:
julia> sumsq(take(countfrom(1), 100))
338350
This is the most idiomatic Julia code, and can handle all sorts of situations. In some other languages, removing type annotations may affect performance, but that is not the case in Julia; only type stability is important for performance.
Imperative factorial
A long-form syntax is available for defining multi-line functions. This can be useful when we use imperative structures such as loops. The expression in tail position is returned. For instance, the below function uses a for
loop to compute the factorial of some integer n
:
function myfactorial(n)
fact = one(n)
for m in 1:n
fact *= m
end
fact
end
Usage:
julia> myfactorial(10)
3628800
In longer functions, it is common to see the return
statement used. The return
statement is not necessary in tail position, but it is still sometimes used for clarity. For instance, another way of writing the above function would be
function myfactorial(n)
fact = one(n)
for m in 1:n
fact *= m
end
return fact
end
which is identical in behaviour to the function above.
Anonymous functions
Arrow syntax
Anonymous functions can be created using the ->
syntax. This is useful for passing functions to higher-order functions, such as the map
function. The below function computes the square of each number in an array A
.
squareall(A) = map(x -> x ^ 2, A)
An example of using this function:
julia> squareall(1:10)
10-element Array{Int64,1}:
1
4
9
16
25
36
49
64
81
100
Multiline syntax
Multiline anonymous functions can be created using function
syntax. For instance, the following example computes the factorials of the first n
numbers, but using an anonymous function instead of the built in factorial
.
julia> map(function (n)
product = one(n)
for i in 1:n
product *= i
end
product
end, 1:10)
10-element Array{Int64,1}:
1
2
6
24
120
720
5040
40320
362880
3628800
Do block syntax
Because it is so common to pass an anonymous function as the first argument to a function, there is a do
block syntax. The syntax
map(A) do x
x ^ 2
end
is equivalent to
map(x -> x ^ 2, A)
but the former can be more clear in many situations, especially if a lot of computation is being done in the anonymous function. do
block syntax is especially useful for file input and output for resource management reasons.