Functions¶
In Julia, a function is an object that maps a tuple of argument values to a return value. Julia functions are not pure mathematical functions, in the sense that functions can alter and be affected by the global state of the program. The basic syntax for defining functions in Julia is:
function f(x,y)x+yend
There is a second, more terse syntax for defining a function in Julia. The traditional function declaration syntax demonstrated above is equivalent to the following compact “assignment form”:
f(x,y)=x+y
In the assignment form, the body of the function must be a single expression, although it can be a compound expression (see Compound Expressions). Short, simple function definitions are common in Julia. The short function syntax is accordingly quite idiomatic, considerably reducing both typing and visual noise.
A function is called using the traditional parenthesis syntax:
julia>f(2,3)5
Without parentheses, the expression f
refers to the function object,
and can be passed around like any value:
julia>g=f;julia>g(2,3)5
As with variables, Unicode can also be used for function names:
julia>∑(x,y)=x+y∑(genericfunction with1method)
Argument Passing Behavior¶
Julia function arguments follow a convention sometimes called “pass-by-sharing”, which means that values are not copied when they are passed to functions. Function arguments themselves act as new variable bindings (new locations that can refer to values), but the values they refer to are identical to the passed values. Modifications to mutable values (such as Arrays) made within a function will be visible to the caller. This is the same behavior found in Scheme, most Lisps, Python, Ruby and Perl, among other dynamic languages.
The return
Keyword¶
The value returned by a function is the value of the last expression
evaluated, which, by default, is the last expression in the body of the
function definition. In the example function, f
, from the previous
section this is the value of the expression x+y
. As in C and most
other imperative or functional languages, the return
keyword causes
a function to return immediately, providing an expression whose value is
returned:
function g(x,y)returnx*yx+yend
Since function definitions can be entered into interactive sessions, it is easy to compare these definitions:
f(x,y)=x+yfunction g(x,y)returnx*yx+yendjulia>f(2,3)5julia>g(2,3)6
Of course, in a purely linear function body like g
, the usage of
return
is pointless since the expression x+y
is never
evaluated and we could simply make x*y
the last expression in the
function and omit the return
. In conjunction with other control
flow, however, return
is of real use. Here, for example, is a
function that computes the hypotenuse length of a right triangle with
sides of length x and y, avoiding overflow:
function hypot(x,y)x=abs(x)y=abs(y)ifx>yr=y/xreturnx*sqrt(1+r*r)endify==0returnzero(x)endr=x/yreturny*sqrt(1+r*r)end
There are three possible points of return from this function, returning
the values of three different expressions, depending on the values of
x and y. The return
on the last line could be omitted since it
is the last expression.
Operators Are Functions¶
In Julia, most operators are just functions with support for special
syntax. (The exceptions are operators with special evaluation semantics
like &&
and ||
. These operators cannot be functions since
short-circuit evaluation requires that
their operands are not evaluated before evaluation of the operator.)
Accordingly, you can also apply them using parenthesized argument lists,
just as you would any other function:
julia>1+2+36julia>+(1,2,3)6
The infix form is exactly equivalent to the function application form —
in fact the former is parsed to produce the function call internally.
This also means that you can assign and pass around operators such as
+()
and *()
just like you would with other function values:
julia>f=+;julia>f(1,2,3)6
Under the name f
, the function does not support infix notation,
however.
Operators With Special Names¶
A few special expressions correspond to calls to functions with non-obvious names. These are:
Expression | Calls |
---|---|
[ABC...] | hcat() |
[A,B,C,...] | vcat() |
[AB;CD;...] | hvcat() |
A' | ctranspose() |
A.' | transpose() |
1:n | colon() |
A[i] | getindex() |
A[i]=x | setindex!() |
These functions are included in the Base.Operators
module even
though they do not have operator-like names.
Anonymous Functions¶
Functions in Julia are first-class objects: they can be assigned to variables, and called using the standard function call syntax from the variable they have been assigned to. They can be used as arguments, and they can be returned as values. They can also be created anonymously, without being given a name, using either of these syntaxes:
julia>x->x^2+2x-1(::#1) (generic function with 1 method)julia>function(x)x^2+2x-1end(::#3) (generic function with 1 method)
This creates a function taking one argument x and returning the value of the polynomial x^2 + 2x - 1 at that value. Notice that the result is a generic function, but with a compiler-generated name based on consecutive numbering.
The primary use for anonymous functions is passing them to functions which take
other functions as arguments. A classic example is map()
,
which applies a function to each value of an array and returns a new
array containing the resulting values:
julia>map(round,[1.2,3.5,1.7])3-elementArray{Float64,1}:1.04.02.0
This is fine if a named function effecting the transform one wants
already exists to pass as the first argument to map()
. Often, however,
a ready-to-use, named function does not exist. In these situations, the
anonymous function construct allows easy creation of a single-use
function object without needing a name:
julia>map(x->x^2+2x-1,[1,3,-1])3-elementArray{Int64,1}:214-2
An anonymous function accepting multiple arguments can be written using
the syntax (x,y,z)->2x+y-z
. A zero-argument anonymous function is
written as ()->3
. The idea of a function with no arguments may seem
strange, but is useful for “delaying” a computation. In this usage, a
block of code is wrapped in a zero-argument function, which is later
invoked by calling it as f()
.
Multiple Return Values¶
In Julia, one returns a tuple of values to simulate returning multiple values. However, tuples can be created and destructured without needing parentheses, thereby providing an illusion that multiple values are being returned, rather than a single tuple value. For example, the following function returns a pair of values:
julia>function foo(a,b)a+b,a*bend;
If you call it in an interactive session without assigning the return value anywhere, you will see the tuple returned:
julia>foo(2,3)(5,6)
A typical usage of such a pair of return values, however, extracts each value into a variable. Julia supports simple tuple “destructuring” that facilitates this:
julia>x,y=foo(2,3);julia>x5julia>y6
You can also return multiple values via an explicit usage of the
return
keyword:
function foo(a,b)returna+b,a*bend
This has the exact same effect as the previous definition of foo
.
Varargs Functions¶
It is often convenient to be able to write functions taking an arbitrary number of arguments. Such functions are traditionally known as “varargs” functions, which is short for “variable number of arguments”. You can define a varargs function by following the last argument with an ellipsis:
julia>bar(a,b,x...)=(a,b,x)bar(genericfunction with1method)
The variables a
and b
are bound to the first two argument values
as usual, and the variable x
is bound to an iterable collection of
the zero or more values passed to bar
after its first two arguments:
julia>bar(1,2)(1,2,())julia>bar(1,2,3)(1,2,(3,))julia>bar(1,2,3,4)(1,2,(3,4))julia>bar(1,2,3,4,5,6)(1,2,(3,4,5,6))
In all these cases, x
is bound to a tuple of the trailing values
passed to bar
.
It is possible to constrain the number of values passed as a variable argument; this will be discussed later in Parametrically-constrained Varargs methods.
On the flip side, it is often handy to “splice” the values contained in
an iterable collection into a function call as individual arguments. To
do this, one also uses ...
but in the function call instead:
julia>x=(3,4)(3,4)julia>bar(1,2,x...)(1,2,(3,4))
In this case a tuple of values is spliced into a varargs call precisely where the variable number of arguments go. This need not be the case, however:
julia>x=(2,3,4)(2,3,4)julia>bar(1,x...)(1,2,(3,4))julia>x=(1,2,3,4)(1,2,3,4)julia>bar(x...)(1,2,(3,4))
Furthermore, the iterable object spliced into a function call need not be a tuple:
julia>x=[3,4]2-elementArray{Int64,1}:34julia>bar(1,2,x...)(1,2,(3,4))julia>x=[1,2,3,4]4-elementArray{Int64,1}:1234julia>bar(x...)(1,2,(3,4))
Also, the function that arguments are spliced into need not be a varargs function (although it often is):
julia>baz(a,b)=a+b;julia>args=[1,2]2-elementArray{Int64,1}:12julia>baz(args...)3julia>args=[1,2,3]3-elementArray{Int64,1}:123julia>baz(args...)ERROR:MethodError:nomethodmatchingbaz(::Int64,::Int64,::Int64)Closestcandidatesare:baz(::Any,::Any)atnone:1...
As you can see, if the wrong number of elements are in the spliced container, then the function call will fail, just as it would if too many arguments were given explicitly.
Optional Arguments¶
In many cases, function arguments have sensible default values and therefore
might not need to be passed explicitly in every call. For example, the
library function parse(type,num,base)
interprets a string as a number
in some base. The base
argument defaults to 10
. This behavior can be
expressed concisely as:
function parse(type,num,base=10)###end
With this definition, the function can be called with either two or three
arguments, and 10
is automatically passed when a third argument is not
specified:
julia>parse(Int,"12",10)12julia>parse(Int,"12",3)5julia>parse(Int,"12")12
Optional arguments are actually just a convenient syntax for writing multiple method definitions with different numbers of arguments (see Note on Optional and keyword Arguments).
Keyword Arguments¶
Some functions need a large number of arguments, or have a large number of behaviors. Remembering how to call such functions can be difficult. Keyword arguments can make these complex interfaces easier to use and extend by allowing arguments to be identified by name instead of only by position.
For example, consider a function plot
that
plots a line. This function might have many options, for controlling line
style, width, color, and so on. If it accepts keyword arguments, a possible
call might look like plot(x,y,width=2)
, where we have chosen to
specify only line width. Notice that this serves two purposes. The call is
easier to read, since we can label an argument with its meaning. It also
becomes possible to pass any subset of a large number of arguments, in
any order.
Functions with keyword arguments are defined using a semicolon in the signature:
function plot(x,y;style="solid",width=1,color="black")###end
When the function is called, the semicolon is optional: one can
either call plot(x,y,width=2)
or plot(x,y;width=2)
, but
the former style is more common. An explicit semicolon is required only
for passing varargs or computed keywords as described below.
Keyword argument default values are evaluated only when necessary (when a corresponding keyword argument is not passed), and in left-to-right order. Therefore default expressions may refer to prior keyword arguments.
The types of keyword arguments can be made explicit as follows:
function f(;x::Int64=1)###end
Extra keyword arguments can be collected using ...
, as in varargs
functions:
function f(x;y=0,kwargs...)###end
Inside f
, kwargs
will be a collection of (key,value)
tuples,
where each key
is a symbol. Such collections can be passed as keyword
arguments using a semicolon in a call, e.g. f(x,z=1;kwargs...)
.
Dictionaries can also be used for this purpose.
One can also pass (key,value)
tuples, or any iterable
expression (such as a =>
pair) that can be assigned to such a
tuple, explicitly after a semicolon. For example, plot(x,y;(:width,2))
and plot(x,y;:width=>2)
are equivalent to
plot(x,y,width=2)
. This is useful in situations where the
keyword name is computed at runtime.
Evaluation Scope of Default Values¶
Optional and keyword arguments differ slightly in how their default values are evaluated. When optional argument default expressions are evaluated, only previous arguments are in scope. In contrast, all the arguments are in scope when keyword arguments default expressions are evaluated. For example, given this definition:
function f(x,a=b,b=1)###end
the b
in a=b
refers to a b
in an outer scope, not the
subsequent argument b
. However, if a
and b
were keyword
arguments instead, then both would be created in the same scope and
the b
in a=b
would refer to the subsequent argument b
(shadowing any b
in an outer scope), which would result in an
undefined variable error (since the default expressions are evaluated
left-to-right, and b
has not been assigned yet).
Do-Block Syntax for Function Arguments¶
Passing functions as arguments to other functions is a powerful technique,
but the syntax for it is not always convenient. Such calls are especially
awkward to write when the function argument requires multiple lines. As
an example, consider calling map()
on a function with several cases:
map(x->beginifx<0&&iseven(x)return0elseifx==0return1elsereturnxendend,[A,B,C])
Julia provides a reserved word do
for rewriting this code more clearly:
map([A,B,C])doxifx<0&&iseven(x)return0elseifx==0return1elsereturnxendend
The dox
syntax creates an anonymous function with argument x
and passes it as the first argument to map()
. Similarly, doa,b
would create a two-argument anonymous function, and a plain do
would declare that what follows is an anonymous function of the form
()->...
.
How these arguments are initialized depends on the “outer” function;
here, map()
will sequentially set x
to A
, B
, C
,
calling the anonymous function on each, just as would happen in the
syntax map(func,[A,B,C])
.
This syntax makes it easier to use functions to effectively extend the
language, since calls look like normal code blocks. There are many
possible uses quite different from map()
, such as managing system
state. For example, there is a version of open()
that runs code
ensuring that the opened file is eventually closed:
open("outfile","w")doiowrite(io,data)end
This is accomplished by the following definition:
function open(f::Function,args...)io=open(args...)tryf(io)finallyclose(io)endend
Here, open()
first opens the file for writing and then passes
the resulting output stream to the anonymous function you defined
in the do...end
block. After your function exits, open()
will make sure that the stream is properly closed, regardless of
whether your function exited normally or threw an exception.
(The try/finally
construct will be described in
Control Flow.)
With the do
block syntax, it helps to check the documentation or
implementation to know how the arguments of the user function are
initialized.
Dot Syntax for Vectorizing Functions¶
In technical-computing languages, it is common to have “vectorized” versions of
functions, which simply apply a given function f(x)
to each element of an
array A
to yield a new array via f(A)
. This kind of syntax is
convenient for data processing, but in other languages vectorization is also
often required for performance: if loops are slow, the “vectorized” version of a
function can call fast library code written in a low-level language. In Julia,
vectorized functions are not required for performance, and indeed it is often
beneficial to write your own loops (see Performance Tips), but they
can still be convenient. Therefore, any Julia function f
can be applied
elementwise to any array (or other collection) with the syntax f.(A)
.
Of course, you can omit the dot if you write a specialized “vector” method
of f
, e.g. via f(A::AbstractArray)=map(f,A)
, and this is just
as efficient as f.(A)
. But that approach requires you to decide in advance
which functions you want to vectorize.
More generally, f.(args...)
is actually equivalent to
broadcast(f,args...)
, which allows you to operate on multiple
arrays (even of different shapes), or a mix of arrays and scalars
(see Broadcasting). For example, if you have f(x,y)=3x+4y
,
then f.(pi,A)
will return a new array consisting of f(pi,a)
for each
a
in A
, and f.(vector1,vector2)
will return a new vector
consisting of f(vector1[i],vector2[i])
for each index i
(throwing an exception if the vectors have different length).
Moreover, nestedf.(args...)
calls are fused into a single broadcast
loop. For example, sin.(cos.(X))
is equivalent to broadcast(x->sin(cos(x)),X)
,
similar to [sin(cos(x))forxinX]
: there is only a single loop over X
,
and a single array is allocated for the result. [In contrast, sin(cos(X))
in a typical “vectorized” language would first allocate one temporary array for tmp=cos(X)
,
and then compute sin(tmp)
in a separate loop, allocating a second array.]
This loop fusion is not a compiler optimization that may or may not occur, it
is a syntactic guarantee whenever nested f.(args...)
calls are encountered. Technically,
the fusion stops as soon as a “non-dot” function is encountered; for example,
in sin.(sort(cos.(X)))
the sin
and cos
loops cannot be merged
because of the intervening sort
function.
Finally, the maximum efficiency is typically achieved when the output
array of a vectorized operation is pre-allocated, so that repeated
calls do not allocate new arrays over and over again for the results
(Pre-allocating outputs:). A convenient syntax for this is
X.=...
, which is equivalent to broadcast!(identity,X,...)
except that, as above, the broadcast!
loop is fused with any nested
“dot” calls. For example, X.=sin.(Y)
is equivalent to
broadcast!(sin,X,Y)
, overwriting X
with sin.(Y)
in-place.
If the left-hand side is an array-indexing expression, e.g.
X[2:end].=sin.(Y)
, then it translates to broadcast!
on a view
,
e.g. broadcast!(sin,view(X,2:endof(X)),Y)
, so that the left-hand
side is updated in-place.
(In future versions of Julia, operators like .*
will also be handled with
the same mechanism: they will be equivalent to broadcast
calls and
will be fused with other nested “dot” calls. X.+=Y
is equivalent
to X.=X.+Y
and will eventually result in a fused in-place assignment.
Similarly for .*=
etcetera.)
Further Reading¶
We should mention here that this is far from a complete picture of defining functions. Julia has a sophisticated type system and allows multiple dispatch on argument types. None of the examples given here provide any type annotations on their arguments, meaning that they are applicable to all types of arguments. The type system is described in Types and defining a function in terms of methods chosen by multiple dispatch on run-time argument types is described in Methods.