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, because they can alter and be affected by the global state of the program. The basic syntax for defining functions in Julia is:
julia> function f(x, y) x + y endf (generic function with 1 method)
This function accepts two argumentsx
andy
and returns the value of the last expression evaluated, which isx + y
.
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":
julia> f(x, y) = x + yf (generic function with 1 method)
In the assignment form, the body of the function must be a single expression, although it can be a compound expression (seeCompound 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 expressionf
refers to the function object, and can be passed around like any other 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∑ (generic function with 1 method)julia> ∑(2, 3)5
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 variablebindings (new "names" that can refer to values), much likeassignmentsargument_name = argument_value
, so that the objects they refer to are identical to the passed values. Modifications to mutable values (such asArray
s) 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.)
For example, in the function
function f(x, y) x[1] = 42 # mutates x y = 7 + y # new binding for y, no mutation return yend
The statementx[1] = 42
mutates the objectx
, and hence this changewill be visible in the array passed by the caller for this argument. On the other hand, the assignmenty = 7 + y
changes thebinding ("name")y
to refer to a new value7 + y
, rather than mutating theoriginal object referred to byy
, and hence doesnot change the corresponding argument passed by the caller. This can be seen if we callf(x, y)
:
julia> a = [4, 5, 6]3-element Vector{Int64}: 4 5 6julia> b = 33julia> f(a, b) # returns 7 + b == 1010julia> a # a[1] is changed to 42 by f3-element Vector{Int64}: 42 5 6julia> b # not changed3
As a common convention in Julia (not a syntactic requirement), such a function wouldtypically be namedf!(x, y)
rather thanf(x, y)
, as a visual reminder at the call site that at least one of the arguments (often the first one) is being mutated.
The behavior of a mutating function can be unexpected when a mutated argument shares memory with another argument, a situation known as aliasing (e.g. when one is a view of the other). Unless the function docstring explicitly indicates that aliasing produces the expected result, it is the responsibility of the caller to ensure proper behavior on such inputs.
You can declare the types of function arguments by appending::TypeName
to the argument name, as usual forType Declarations in Julia. For example, the following function computesFibonacci numbers recursively:
fib(n::Integer) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)
and the::Integer
specification means that it will only be callable whenn
is a subtype of theabstractInteger
type.
Argument-type declarationsnormally have no impact on performance: regardless of what argument types (if any) are declared, Julia compiles a specialized version of the function for the actual argument types passed by the caller. For example, callingfib(1)
will trigger the compilation of specialized version offib
optimized specifically forInt
arguments, which is then re-used iffib(7)
orfib(15)
are called. (There are rare exceptions when an argument-type declaration can trigger additional compiler specializations; see:Be aware of when Julia avoids specializing.) The most common reasons to declare argument types in Julia are, instead:
fib(x::Number) = ...
that works for anyNumber
type by usingBinet's formula to extend it to non-integer values.fib(n) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)
, thenfib(1.5)
would silently give us the nonsensical answer1.0
.However, it is acommon mistake to overly restrict the argument types, which can unnecessarily limit the applicability of the function and prevent it from being re-used in circumstances you did not anticipate. For example, thefib(n::Integer)
function above works equally well forInt
arguments (machine integers) andBigInt
arbitrary-precision integers (seeBigFloats and BigInts), which is especially useful because Fibonacci numbers grow exponentially rapidly and will quickly overflow any fixed-precision type likeInt
(seeOverflow behavior). If we had declared our function asfib(n::Int)
, however, the application toBigInt
would have been prevented for no reason. In general, you should use the most general applicable abstract types for arguments, andwhen in doubt, omit the argument types. You can always add argument-type specifications later if they become necessary, and you don't sacrifice performance or functionality by omitting them.
return
KeywordThe 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 expressionx + y
. As an alternative, as in many other languages, thereturn
keyword causes a function to return immediately, providing an expression whose value is returned:
function g(x, y) return x * y x + yend
Since function definitions can be entered into interactive sessions, it is easy to compare these definitions:
julia> f(x, y) = x + yf (generic function with 1 method)julia> function g(x, y) return x * y x + y endg (generic function with 1 method)julia> f(2, 3)5julia> g(2, 3)6
Of course, in a purely linear function body likeg
, the usage ofreturn
is pointless since the expressionx + y
is never evaluated and we could simply makex * y
the last expression in the function and omit thereturn
. 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 lengthx
andy
, avoiding overflow:
julia> function hypot(x, y) x = abs(x) y = abs(y) if x > y r = y/x return x*sqrt(1 + r*r) end if y == 0 return zero(x) end r = x/y return y*sqrt(1 + r*r) endhypot (generic function with 1 method)julia> hypot(3, 4)5.0
There are three possible points of return from this function, returning the values of three different expressions, depending on the values ofx
andy
. Thereturn
on the last line could be omitted since it is the last expression.
A return type can be specified in the function declaration using the::
operator. This converts the return value to the specified type.
julia> function g(x, y)::Int8 return x * y end;julia> typeof(g(1, 2))Int8
This function will always return anInt8
regardless of the types ofx
andy
. SeeType Declarations for more on return types.
Return type declarations arerarely used in Julia: in general, you should instead write "type-stable" functions in which Julia's compiler can automatically infer the return type. For more information, see thePerformance Tips chapter.
For functions that do not need to return a value (functions used only for some side effects), the Julia convention is to return the valuenothing
:
function printx(x) println("x = $x") return nothingend
This is aconvention in the sense thatnothing
is not a Julia keyword but only a singleton object of typeNothing
. Also, you may notice that theprintx
function example above is contrived, becauseprintln
already returnsnothing
, so that thereturn
line is redundant.
There are two possible shortened forms for thereturn nothing
expression. On the one hand, thereturn
keyword implicitly returnsnothing
, so it can be used alone. On the other hand, since functions implicitly return their last expression evaluated,nothing
can be used alone when it's the last expression. The preference for the expressionreturn nothing
as opposed toreturn
ornothing
alone is a matter of coding style.
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 sinceShort-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 namef
, the function does not support infix notation, however.
A few special expressions correspond to calls to functions with non-obvious names. These are:
Expression | Calls |
---|---|
[A B C ...] | hcat |
[A; B; C; ...] | vcat |
[A B; C D; ...] | hvcat |
[A; B;; C; D;; ...] | hvncat |
A' | adjoint |
A[i] | getindex |
A[i] = x | setindex! |
A.n | getproperty |
A.n = x | setproperty! |
Note that expressions similar to[A; B;; C; D;; ...]
but with more than two consecutive;
also correspond tohvncat
calls.
Functions in Julia arefirst-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 - 1 end#3 (generic function with 1 method)
Each statement creates a function taking one argumentx
and returning the value of the polynomialx^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 ismap
, 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-element Vector{Float64}: 1.0 4.0 2.0
This is fine if a named function effecting the transform already exists to pass as the first argument tomap
. 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-element Vector{Int64}: 2 14 -2
An anonymous function accepting multiple arguments can be written using the syntax(x,y,z)->2x+y-z
.
Argument-type declarations for anonymous functions work as for named functions, for examplex::Integer->2x
. The return type of an anonymous function cannot be specified.
A zero-argument anonymous function can be written as()->2+2
. The idea of a function with no arguments may seem strange, but is useful in cases where a result cannot (or should not) be precomputed. For example, Julia has a zero-argumenttime
function that returns the current time in seconds, and thusseconds = ()->round(Int, time())
is an anonymous function that returns this time rounded to the nearest integer assigned to the variableseconds
. Each time this anonymous function is called asseconds()
the current time will be calculated and returned.
Julia has a built-in data structure called atuple that is closely related to function arguments and return values. A tuple is a fixed-length container that can hold any values, but cannot be modified (it isimmutable). Tuples are constructed with commas and parentheses, and can be accessed via indexing:
julia> (1, 1+1)(1, 2)julia> (1,)(1,)julia> x = (0.0, "hello", 6*7)(0.0, "hello", 42)julia> x[2]"hello"
Notice that a length-1 tuple must be written with a comma,(1,)
, since(1)
would just be a parenthesized value.()
represents the empty (length-0) tuple.
The components of tuples can optionally be named, in which case anamed tuple is constructed:
julia> x = (a=2, b=1+2)(a = 2, b = 3)julia> x[1]2julia> x.a2
The fields of named tuples can be accessed by name using dot syntax (x.a
) in addition to the regular indexing syntax (x[1]
orx[:a]
).
A comma-separated list of variables (optionally wrapped in parentheses) can appear on the left side of an assignment: the value on the right side isdestructured by iterating over and assigning to each variable in turn:
julia> (a, b, c) = 1:31:3julia> b2
The value on the right should be an iterator (seeIteration interface) at least as long as the number of variables on the left (any excess elements of the iterator are ignored).
This can be used to return multiple values from functions by returning a tuple or other iterable value. For example, the following function returns two values:
julia> function foo(a, b) a+b, a*b endfoo (generic function with 1 method)
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)
Destructuring assignment extracts each value into a variable:
julia> x, y = foo(2, 3)(5, 6)julia> x5julia> y6
Another common use is for swapping variables:
julia> y, x = x, y(5, 6)julia> x6julia> y5
If only a subset of the elements of the iterator are required, a common convention is to assign ignored elements to a variable consisting of only underscores_
(which is an otherwise invalid variable name, seeAllowed Variable Names):
julia> _, _, _, d = 1:101:10julia> d4
Other valid left-hand side expressions can be used as elements of the assignment list, which will callsetindex!
orsetproperty!
, or recursively destructure individual elements of the iterator:
julia> X = zeros(3);julia> X[1], (a, b) = (1, (2, 3))(1, (2, 3))julia> X3-element Vector{Float64}: 1.0 0.0 0.0julia> a2julia> b3
...
with assignment requires Julia 1.6
If the last symbol in the assignment list is suffixed by...
(known asslurping), then it will be assigned a collection or lazy iterator of the remaining elements of the right-hand side iterator:
julia> a, b... = "hello""hello"julia> a'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)julia> b"ello"julia> a, b... = Iterators.map(abs2, 1:4)Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4)julia> a1julia> bBase.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4), 1)
SeeBase.rest
for details on the precise handling and customization for specific iterators.
...
in non-final position of an assignment requires Julia 1.9
Slurping in assignments can also occur in any other position. As opposed to slurping the end of a collection however, this will always be eager.
julia> a, b..., c = 1:51:5julia> a1julia> b3-element Vector{Int64}: 2 3 4julia> c5julia> front..., tail = "Hi!""Hi!"julia> front"Hi"julia> tail'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)
This is implemented in terms of the functionBase.split_rest
.
Note that for variadic function definitions, slurping is still only allowed in final position. This does not apply tosingle argument destructuring though, as that does not affect method dispatch:
julia> f(x..., y) = xERROR: syntax: invalid "..." on non-final argumentStacktrace:[...]julia> f((x..., y)) = xf (generic function with 1 method)julia> f((1, 2, 3))(1, 2)
Instead of destructuring based on iteration, the right side of assignments can also be destructured using property names. This follows the syntax for NamedTuples, and works by assigning to each variable on the left a property of the right side of the assignment with the same name usinggetproperty
:
julia> (; b, a) = (a=1, b=2, c=3)(a = 1, b = 2, c = 3)julia> a1julia> b2
The destructuring feature can also be used within a function argument. If a function argument name is written as a tuple (e.g.(x, y)
) instead of just a symbol, then an assignment(x, y) = argument
will be inserted for you:
julia> minmax(x, y) = (y < x) ? (y, x) : (x, y)julia> gap((min, max)) = max - minjulia> gap(minmax(10, 2))8
Notice the extra set of parentheses in the definition ofgap
. Without those,gap
would be a two-argument function, and this example would not work.
Similarly, property destructuring can also be used for function arguments:
julia> foo((; x, y)) = x + yfoo (generic function with 1 method)julia> foo((x=1, y=2))3julia> struct A x y endjulia> foo(A(3, 4))7
For anonymous functions, destructuring a single argument requires an extra comma:
julia> map(((x, y),) -> x + y, [(1, 2), (3, 4)])2-element Array{Int64,1}: 3 7
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 positional argument with an ellipsis:
julia> bar(a, b, x...) = (a, b, x)bar (generic function with 1 method)
The variablesa
andb
are bound to the first two argument values as usual, and the variablex
is bound to an iterable collection of the zero or more values passed tobar
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 tobar
.
It is possible to constrain the number of values passed as a variable argument; this will be discussed later inParametrically-constrained Varargs methods.
On the flip side, it is often handy to "splat" 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 splatted into a function call need not be a tuple:
julia> x = [3, 4]2-element Vector{Int64}: 3 4julia> bar(1, 2, x...)(1, 2, (3, 4))julia> x = [1, 2, 3, 4]4-element Vector{Int64}: 1 2 3 4julia> bar(x...)(1, 2, (3, 4))
Also, the function that arguments are splatted into need not be a varargs function (although it often is):
julia> baz(a, b) = a + b;julia> args = [1, 2]2-element Vector{Int64}: 1 2julia> baz(args...)3julia> args = [1, 2, 3]3-element Vector{Int64}: 1 2 3julia> baz(args...)ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)The function `baz` exists, but no method is defined for this combination of argument types.Closest candidates are: baz(::Any, ::Any) @ Main none:1Stacktrace:[...]
As you can see, if the wrong number of elements are in the splatted container, then the function call will fail, just as it would if too many arguments were given explicitly.
It is often possible to provide sensible default values for function arguments. This can save users from having to pass every argument on every call. For example, the functionDate(y, [m, d])
fromDates
module constructs aDate
type for a given yeary
, monthm
and dayd
. However,m
andd
arguments are optional and their default value is1
. This behavior can be expressed concisely as:
julia> using Datesjulia> function date(y::Int64, m::Int64=1, d::Int64=1) err = Dates.validargs(Date, y, m, d) err === nothing || throw(err) return Date(Dates.UTD(Dates.totaldays(y, m, d))) enddate (generic function with 3 methods)
Observe, that this definition calls another method of theDate
function that takes one argument of typeUTInstant{Day}
.
With this definition, the function can be called with either one, two or three arguments, and1
is automatically passed when only one or two of the arguments are specified:
julia> date(2000, 12, 12)2000-12-12julia> date(2000, 12)2000-12-01julia> date(2000)2000-01-01
Optional arguments are actually just a convenient syntax for writing multiple method definitions with different numbers of arguments (seeNote on Optional and keyword Arguments). This can be checked for ourdate
function example by calling themethods
function:
julia> methods(date)# 3 methods for generic function "date":[1] date(y::Int64) in Main at REPL[1]:1[2] date(y::Int64, m::Int64) in Main at REPL[1]:1[3] date(y::Int64, m::Int64, d::Int64) in Main at REPL[1]:1
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 functionplot
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 likeplot(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 callplot(x, y, width=2)
orplot(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::Int=1) ###end
Keyword arguments can also be used in varargs functions:
function plot(x...; style="solid") ###end
Extra keyword arguments can be collected using...
, as in varargs functions:
function f(x; y=0, kwargs...) ###end
Insidef
,kwargs
will be an immutable key-value iterator over a named tuple. Named tuples (as well as dictionaries with keys ofSymbol
, and other iterators yielding two-value collections with symbol as first values) can be passed as keyword arguments using a semicolon in a call, e.g.f(x, z=1; kwargs...)
.
If a keyword argument is not assigned a default value in the method definition, then it isrequired: anUndefKeywordError
exception will be thrown if the caller does not assign it a value:
function f(x; y) ###endf(3, y=5) # ok, y is assignedf(3) # throws UndefKeywordError(:y)
One can also passkey => value
expressions after a semicolon. For example,plot(x, y; :width => 2)
is equivalent toplot(x, y, width=2)
. This is useful in situations where the keyword name is computed at runtime.
When a bare identifier or dot expression occurs after a semicolon, the keyword argument name is implied by the identifier or field name. For exampleplot(x, y; width)
is equivalent toplot(x, y; width=width)
andplot(x, y; options.width)
is equivalent toplot(x, y; width=options.width)
.
The nature of keyword arguments makes it possible to specify the same argument more than once. For example, in the callplot(x, y; options..., width=2)
it is possible that theoptions
structure also contains a value forwidth
. In such a case the rightmost occurrence takes precedence; in this example,width
is certain to have the value2
. However, explicitly specifying the same keyword argument multiple times, for exampleplot(x, y, width=2, width=3)
, is not allowed and results in a syntax error.
When optional and keyword argument default expressions are evaluated, onlyprevious arguments are in scope. For example, given this definition:
function f(x, a=b, b=1) ###end
theb
ina=b
refers to ab
in an outer scope, not the subsequent argumentb
.
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 callingmap
on a function with several cases:
map(x->begin if x < 0 && iseven(x) return 0 elseif x == 0 return 1 else return x end end, [A, B, C])
Julia provides a reserved worddo
for rewriting this code more clearly:
map([A, B, C]) do x if x < 0 && iseven(x) return 0 elseif x == 0 return 1 else return x endend
Thedo x
syntax creates an anonymous function with argumentx
and passes the anonymous function as the first argument to the "outer" function -map
in this example. Similarly,do a,b
would create a two-argument anonymous function. Note thatdo (a,b)
would create a one-argument anonymous function, whose argument is a tuple to be deconstructed. A plaindo
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 setx
toA
,B
,C
, calling the anonymous function on each, just as would happen in the syntaxmap(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 frommap
, such as managing system state. For example, there is a version ofopen
that runs code ensuring that the opened file is eventually closed:
open("outfile", "w") do io write(io, data)end
This is accomplished by the following definition:
function open(f::Function, args...) io = open(args...) try f(io) finally close(io) endend
Here,open
first opens the file for writing and then passes the resulting output stream to the anonymous function you defined in thedo ... 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. (Thetry/finally
construct will be described inControl Flow.)
With thedo
block syntax, it helps to check the documentation or implementation to know how the arguments of the user function are initialized.
Ado
block, like any other inner function, can "capture" variables from its enclosing scope. For example, the variabledata
in the above example ofopen...do
is captured from the outer scope. Captured variables can create performance challenges as discussed inperformance tips.
Functions in Julia can be combined by composing or piping (chaining) them together.
Function composition is when you combine functions together and apply the resulting composition to arguments. You use the function composition operator (∘
) to compose the functions, so(f ∘ g)(args...; kw...)
is the same asf(g(args...; kw...))
.
You can type the composition operator at the REPL and suitably-configured editors using\circ<tab>
.
For example, thesqrt
and+
functions can be composed like this:
julia> (sqrt ∘ +)(3, 6)3.0
This adds the numbers first, then finds the square root of the result.
The next example composes three functions and maps the result over an array of strings:
julia> map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))6-element Vector{Char}: 'U': ASCII/Unicode U+0055 (category Lu: Letter, uppercase) 'N': ASCII/Unicode U+004E (category Lu: Letter, uppercase) 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase) 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase) 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase) 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
Function chaining (sometimes called "piping" or "using a pipe" to send data to a subsequent function) is when you apply a function to the previous function's output:
julia> 1:10 |> sum |> sqrt7.416198487095663
Here, the total produced bysum
is passed to thesqrt
function. The equivalent composition would be:
julia> (sqrt ∘ sum)(1:10)7.416198487095663
The pipe operator can also be used with broadcasting, as.|>
, to provide a useful combination of the chaining/piping and dot vectorization syntax (described below).
julia> ["a", "list", "of", "strings"] .|> [uppercase, reverse, titlecase, length]4-element Vector{Any}: "A" "tsil" "Of" 7
When combining pipes with anonymous functions, parentheses must be used if subsequent pipes are not to be parsed as part of the anonymous function's body. Compare:
julia> 1:3 .|> (x -> x^2) |> sum |> sqrt3.7416573867739413julia> 1:3 .|> x -> x^2 |> sum |> sqrt3-element Vector{Float64}: 1.0 2.0 3.0
In technical-computing languages, it is common to have "vectorized" versions of functions, which simply apply a given functionf(x)
to each element of an arrayA
to yield a new array viaf(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 arenot required for performance, and indeed it is often beneficial to write your own loops (seePerformance Tips), but they can still be convenient. Therefore,any Julia functionf
can be applied elementwise to any array (or other collection) with the syntaxf.(A)
. For example,sin
can be applied to all elements in the vectorA
like so:
julia> A = [1.0, 2.0, 3.0]3-element Vector{Float64}: 1.0 2.0 3.0julia> sin.(A)3-element Vector{Float64}: 0.8414709848078965 0.9092974268256817 0.1411200080598672
Of course, you can omit the dot if you write a specialized "vector" method off
, e.g. viaf(A::AbstractArray) = map(f, A)
, and this is just as efficient asf.(A)
. The advantage of thef.(A)
syntax is that which functions are vectorizable need not be decided upon in advance by the library writer.
More generally,f.(args...)
is actually equivalent tobroadcast(f, args...)
, which allows you to operate on multiple arrays (even of different shapes), or a mix of arrays and scalars (seeBroadcasting). For example, if you havef(x, y) = 3x + 4y
, thenf.(pi, A)
will return a new array consisting off(pi,a)
for eacha
inA
, andf.(vector1, vector2)
will return a new vector consisting off(vector1[i], vector2[i])
for each indexi
(throwing an exception if the vectors have different length).
julia> f(x, y) = 3x + 4y;julia> A = [1.0, 2.0, 3.0];julia> B = [4.0, 5.0, 6.0];julia> f.(pi, A)3-element Vector{Float64}: 13.42477796076938 17.42477796076938 21.42477796076938julia> f.(A, B)3-element Vector{Float64}: 19.0 26.0 33.0
Keyword arguments are not broadcasted over, but are simply passed through to each call of the function. For example,round.(x, digits=3)
is equivalent tobroadcast(x -> round(x, digits=3), x)
.
Moreover,nestedf.(args...)
calls arefused into a singlebroadcast
loop. For example,sin.(cos.(X))
is equivalent tobroadcast(x -> sin(cos(x)), X)
, similar to[sin(cos(x)) for x in X]
: there is only a single loop overX
, 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 fortmp=cos(X)
, and then computesin(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 asyntactic guarantee whenever nestedf.(args...)
calls are encountered. Technically, the fusion stops as soon as a "non-dot" function call is encountered; for example, insin.(sort(cos.(X)))
thesin
andcos
loops cannot be merged because of the interveningsort
function.
Finally, the maximum efficiency is typically achieved when the output array of a vectorized operation ispre-allocated, so that repeated calls do not allocate new arrays over and over again for the results (seePre-allocating outputs). A convenient syntax for this isX .= ...
, which is equivalent tobroadcast!(identity, X, ...)
except that, as above, thebroadcast!
loop is fused with any nested "dot" calls. For example,X .= sin.(Y)
is equivalent tobroadcast!(sin, X, Y)
, overwritingX
withsin.(Y)
in-place. If the left-hand side is an array-indexing expression, e.g.X[begin+1:end] .= sin.(Y)
, then it translates tobroadcast!
on aview
, e.g.broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y)
, so that the left-hand side is updated in-place.
Since adding dots to many operations and function calls in an expression can be tedious and lead to code that is difficult to read, the macro@.
is provided to convertevery function call, operation, and assignment in an expression into the "dotted" version.
julia> Y = [1.0, 2.0, 3.0, 4.0];julia> X = similar(Y); # pre-allocate output arrayjulia> @. X = sin(cos(Y)) # equivalent to X .= sin.(cos.(Y))4-element Vector{Float64}: 0.5143952585235492 -0.4042391538522658 -0.8360218615377305 -0.6080830096407656
Binary (or unary) operators like.+
are handled with the same mechanism: they are equivalent tobroadcast
calls and are fused with other nested "dot" calls.X .+= Y
etcetera is equivalent toX .= X .+ Y
and results in a fused in-place assignment; see alsodot operators.
You can also combine dot operations with function chaining using|>
, as in this example:
julia> 1:5 .|> [x->x^2, inv, x->2*x, -, isodd]5-element Vector{Real}: 1 0.5 6 -4 true
All functions in the fused broadcast are always called for every element of the result. ThusX .+ σ .* randn.()
will add a mask of independent and identically sampled random values to each element of the arrayX
, butX .+ σ .* randn()
will add thesame random sample to each element. In cases where the fused computation is constant along one or more axes of the broadcast iteration, it may be possible to leverage a space-time tradeoff and allocate intermediate values to reduce the number of computations. See more atperformance tips.
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 inTypes and defining a function in terms of methods chosen by multiple dispatch on run-time argument types is described inMethods.
Settings
This document was generated withDocumenter.jl version 1.8.0 onWednesday 9 July 2025. Using Julia version 1.11.6.