Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add basic overview of when to use type declarations #39812
add basic overview of when to use type declarations #39812
Changes from all commits
7944ea9
1afedcb
8fb446f
0ecad88
2d936b6
af81f54
cdbeabf
6138102
File filter
Filter by extension
Conversations
Jump to
There are no files selected for viewing
[Functions](@id man-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, because they can alter and be affected by the global state of the program. The basic syntax for defining functions in Julia is:
This function accepts two arguments
x
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":
In the assignment form, the body of the function must be a single expression, although it can be a compound expression (see [Compound Expressions](@ref man-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:
Without parentheses, the expression
f
refers to the function object, and can be passed around like any other value:As with variables, Unicode can also be used for function names:
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
Array
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.Argument-type declarations
You can declare the types of function arguments by appending
::TypeName
to the argument name, as usual for Type Declarations in Julia. For example, the following function computes Fibonacci numbers recursively:and the
::Integer
specification means that it will only be callable whenn
is a subtype of the [abstract](@ref man-abstract-types)Integer
type.Argument-type declarations normally 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, calling
fib(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 using Binet'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 a common 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, the
fib(n::Integer)
function above works equally well forInt
arguments (machine integers) andBigInt
arbitrary-precision integers (see BigFloats and BigInts), which is especially useful because Fibonacci numbers grow exponentially rapidly and will quickly overflow any fixed-precision type likeInt
(see Overflow 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, and when 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.The
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:Since function definitions can be entered into interactive sessions, it is easy to compare these definitions:
Of course, in a purely linear function body like
g
, 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:There are three possible points of return from this function, returning the values of three different expressions, depending on the values of
x
andy
. Thereturn
on the last line could be omitted since it is the last expression.Return type
A return type can be specified in the function declaration using the
::
operator. This converts the return value to the specified type.This function will always return an
Int8
regardless of the types ofx
andy
. See Type Declarations for more on return types.Return type declarations are rarely 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 the [Performance Tips](@ref man-performance-tips) chapter.
Returning nothing
For functions that do not need to return a value (functions used only for some side effects), the Julia convention is to return the value
nothing
:This is a convention in the sense that
nothing
is not a Julia keyword but a only 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 the
return 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.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: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: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:
[A B C ...]
hcat
[A; B; C; ...]
vcat
[A B; C D; ...]
hvcat
A'
adjoint
A[i]
getindex
A[i] = x
setindex!
A.n
getproperty
](@ref Base.getproperty)A.n = x
setproperty!
](@ref Base.setproperty!)[Anonymous Functions](@id man-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:
This creates a function taking one argument
x
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 is
map
, which applies a function to each value of an array and returns a new array containing the resulting values:This is fine if a named function effecting the transform 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: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 asf
.As an example, consider this call to
get
:The code above is equivalent to calling
get
with an anonymous function containing the code enclosed betweendo
andend
, like so:The call to
time
is delayed by wrapping it in a 0-argument anonymous function that is called only when the requested key is absent fromdict
.Tuples
Julia has a built-in data structure called a tuple 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 is immutable). Tuples are constructed with commas and parentheses, and can be accessed via indexing:
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.Named Tuples
The components of tuples can optionally be named, in which case a named tuple is constructed:
Named tuples are very similar to tuples, except that fields can additionally be accessed by name using dot syntax (
x.a
) in addition to the regular indexing syntax (x[1]
).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:
If you call it in an interactive session without assigning the return value anywhere, you will see the tuple returned:
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:
You can also return multiple values using the
return
keyword:This has the exact same effect as the previous definition of
foo
.Argument destructuring
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:Notice the extra set of parentheses in the definition of
gap
. Without those,gap
would be a two-argument function, and this example would not work.For anonymous functions, destructuring a single tuple requires an extra comma:
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 positional argument with an ellipsis:
The variables
a
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: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 in Parametrically-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: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:
Furthermore, the iterable object splatted into a function call need not be a tuple:
Also, the function that arguments are splatted into need not be a varargs function (although it often is):
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.
Optional Arguments
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 function
Date(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:Observe, that this definition calls another method of the
Date
function that takes one argument of typeUTInstant{Day}
.With this definition, the function can be called with either one, two or three arguments, and
1
is automatically passed when only one or two of the arguments are specified: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). This can be checked for our
Date
function example by callingmethods
function.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 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:
When the function is called, the semicolon is optional: one can either call
plot(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:
Keyword arguments can also be used in varargs functions:
Extra keyword arguments can be collected using
...
, as in varargs functions:Inside
f
,kwargs
will be an immutable key-value iterator over a named tuple. Named tuples (as well as dictionaries with keys ofSymbol
) 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 is required: an
UndefKeywordError
exception will be thrown if the caller does not assign it a value:One can also pass
key => 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 example
plot(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 call
plot(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.Evaluation Scope of Default Values
When optional and keyword argument default expressions are evaluated, only previous arguments are in scope. For example, given this definition:
the
b
ina=b
refers to ab
in an outer scope, not the subsequent argumentb
.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:Julia provides a reserved word
do
for rewriting this code more clearly:The
do x
syntax creates an anonymous function with argumentx
and passes it as the first argument tomap
. 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 from
map
, such as managing system state. For example, there is a version ofopen
that runs code ensuring that the opened file is eventually closed:This is accomplished by the following definition:
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 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.A
do
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 in [performance tips](@ref man-performance-captured).Function composition and piping
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...)
is the same asf(g(args...))
.You can type the composition operator at the REPL and suitably-configured editors using
\circ<tab>
.For example, the
sqrt
and+
functions can be composed like this: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:
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:
Here, the total produced by
sum
is passed to thesqrt
function. The equivalent composition would be:The pipe operator can also be used with broadcasting, as
.|>
, to provide a useful combination of the chaining/piping and dot vectorization syntax (described next).[Dot Syntax for Vectorizing Functions](@id man-vectorized)
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 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 are not required for performance, and indeed it is often beneficial to write your own loops (see [Performance Tips](@ref man-performance-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:Of course, you can omit the dot if you write a specialized "vector" method of
f
, 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 (see Broadcasting). 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).Moreover, nested
f.(args...)
calls are fused 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 a syntactic 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 is pre-allocated, so that repeated calls do not allocate new arrays over and over again for the results (see Pre-allocating outputs). A convenient syntax for this is
X .= ...
, 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 [
@.
](@ref @dot) is provided to convert every function call, operation, and assignment in an expression into the "dotted" version.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 also [dot operators](@ref man-dot-operators).You can also combine dot operations with function chaining using
|>
, as in this example: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](@ref man-types) and defining a function in terms of methods chosen by multiple dispatch on run-time argument types is described in Methods.