An example of functions without arguments is Pkg.update()
, which was introduced when we studied packages.
<function>
or <operator>
).
This is just notation, and the symbols <
and >
should not be misconstrued as Julia's syntax.
Action | Keyboard Shortcut |
---|---|
Previous Section | Ctrl + ๐ |
Next Section | Ctrl + ๐ |
List of Sections | Ctrl + z |
List of Subsections | Ctrl + x |
Close Any Popped Up Window (like this one) | Esc |
Open All Codes and Outputs in a Post | Alt + ๐ |
Close All Codes and Outputs in a Post | Alt + ๐ |
Unit | Acronym | Measure in Seconds |
---|---|---|
Seconds | s | 1 |
Milliseconds | ms | 10-3 |
Microseconds | ฮผs | 10-6 |
Nanoseconds | ns | 10-9 |
Recall that functions can be classified into i) built-in functions, ii) third-party functions, and iii) user-defined functions. The previous section has covered the first two, and we now focus on iii).
The first step to define your own functions is giving names. Function names follow similar rules to variable names. In particular, they accept Unicode characters, enabling the user to define functions such as โ(x)
. Once you create them, functions can be called without invoking any prefix. This means that a function foo
can be called by simply executing foo(x)
. [note] The method to call a function actually depends on the module in which it's defined, and whether this module has been "imported" or "used". We won't cover modules on this website. However, they're essential when working for large projects, as each module operates as an independent workspace with its own variables. When initiating a new session in Julia, you're actually working within a module called Main
.
There are two approaches to defining functions. We'll refer to each as the standard form and the compact form. The standard form is the most general and allows you to write both short and long functions. On the other hand, the compact form is employed for single-line functions and is reminiscent of mathematical definitions. To illustrate each form, consider a function foo
that sums two variables x
and y
.
function foo(x,y)
x + y
end
foo(x,y) = x + y
The output of the compact form is given by the only operation contained. For its part, the standard form defaults to returning the last line as its output, allowing you to specify the output via the keyword return
. To return multiple outputs, you can use a collection, with tuples being the most common choice. [note] The reason for this is that tuples are more performant than vectors when the number of elements is small.
These approaches to specifying an output are illustrated below.
function foo(x,y)
term1 = x + y
term2 = x * y
return term2
end
foo(10,2)
function foo(x,y)
term1 = x + y
term2 = x * y # output returned
end
foo(10,2)
function foo(x,y)
term1 = x + y
term2 = x * y
return term1, term2 # a tuple, using the notation that omits the parentheses
end
foo(10,2)
function foo(x,y)
term1 = x + y
term2 = x * y
return term1 + term2
end
foo(10,2)
function foo()
a = 1
b = 1
return a + b
end
An example of functions without arguments is Pkg.update()
, which was introduced when we studied packages.
foo1(x) = 2 + foo2(x)
foo2(x) = 1 + x
foo1(2)
foo2(x) = 1 + x
foo1(x) = 2 + foo2(x)
foo1(2)
Up to this point, we've been defining and calling functions using the notation foo(x,y)
. A key characteristic of this syntax is that arguments are passed in a specific order, so that foo(2,4)
assigns the first argument to x
and the second to y
. This approach is known as positional arguments.
However, a major drawback of positional arguments is their susceptibility to silent errors: if we accidentally swap the positions of the arguments, the function may still provide an output. As the number of arguments grows, the likelihood of introducing such bugs increases, making it more challenging to identify and resolve errors.
To circumvent this issue, we can rely on keyword arguments. This approach requires function calls to explicitly specify their arguments, making their order irrelevant. For example, foo(x=2,y=4)
and foo(y=4,x=2)
would then be valid and equivalent.
The following examples illustrate how to define and call functions using both positional and keyword arguments. Additionally, we'll establish that the approaches can be combined. Note that positional arguments necessarily require a semicolon during function definitions, but accept either a semicolon or a comma during function calls.
foo(x, y) = x + y
foo(1,2)
foo(; x, y) = x + y
foo(x=1,y=1)
foo(; x=1, y=1)
# alternative notation (only for calling 'foo')foo(x; y) = x + y
foo(1 ; y=1)
foo(1 , y=1)
# alternative notation
Keyword arguments accept default values, allowing users to omit certain arguments when the function is called. The following examples illustrate how this feature works in practice, where the omitted arguments take on their default values.
foo(x; y=1) = x + y
foo(1)
# equivalent to foo(1,y=1)foo(; x=1, y=1) = x + y
foo()
# equivalent to foo(x=1,y=1)foo(x=2)
# equivalent to foo(x=2,y=1)
When a function is called, its arguments are evaluated sequentially from left to right. This property enables users to define subsequent arguments in terms of previous ones. For example, given foo(;x,y)
, the default value of y
could be set based on the value of x
.
foo(; x, y = x+1) = x + y
foo(x=2)
#function run with implicit value 'y=3'
Given a function foo(x,y)
, you can set the values of x
and y
through a tuple or vector z
. The implementation relies on the splat operator ...
, which unpacks the individual elements of a collection and passes them as separate arguments.
foo(x,y) = x + y
z = (2,3)
foo(z...)
foo(x,y) = x + y
z = [2,3]
foo(z...)
Anonymous functions offer a third way to define functions. Unlike the previous methods, they're commonly introduced with a different purpose: to serve as inputs to other functions. [note] Anonymous functions are also known as lambda functions in other languages.
As the name suggests, anonymous functions aren't referenced by a name. Their syntax resembles the arrow notation from mathematics (e.g. \(x\mapsto \sqrt{x}\)). Specifically, single-argument functions are expressed as x -> <body of the function>
. Likewise, functions with two or more arguments are expressed by (x,y) -> <body of the function>
.
To demonstrate the role of anonymous functions, let's consider the built-in function map(<function>, <collection>)
. This applies <function>
element-wise to each element of <collection>
. For example, map(add_two, x)
applies the function add_two(a) = a + 2
to each element of x = [1,2,3]
, thus returning [3,4,5]
. Applying map
in this way requires defining add_two
beforehand, which unnecessarily pollutes the namespace if add_two
won't be reused. Anonymous functions provide an elegant solution, by directly embedding the operation within map
. In this way, an anonymous function effectively eliminates the need of creating a temporary function like add_two
.
x = [1, 2, 3]
add_two(a) = a + 2
result = map(add_two, x)
result
x = [1, 2, 3]
result = map(a -> a + 2, x)
result
The function map
can also demonstrate the syntax of anonymous functions with multiple arguments. In those cases, the syntax becomes map(<function>, <array1>, <array2>)
. For instance, map(+, [1,2], [2,4])
provides the sum of each pair of numbers, yielding [3,6]
.
x = [1,2,3]
y = [4,5,6]
add(a,b) = a + b
result = map(add_two, x, y)
result
x = [1,2,3]
y = [4,5,6]
result = map((a,b) -> a + b, x, y)
result
Anonymous functions can help keep our code tidy, but they may not be practical for functions that span multiple lines. This inconvenience can be addressed by what's known as do-blocks. They allow us to insert the anonymous function separately, and then pass it as the first argument to a function call. Given a function foo(<inner function>, <vector>)
, its generic implementation is as follows.
foo(<vector>) do <arguments of inner function>
# body of inner function
end
To illustrate the notation with a concrete scenario, let's revisit the map
function example and rewrite it using a do-block.
x = [1, 2, 3]
add_two(a) = a + 2
result = map(add_two, x)
result
x = [1, 2, 3]
result = map(a -> a + 2, x)
result
x = [1, 2, 3]
result = map(x) do a
a + 2
end
result
Do-blocks also accept anonymous functions with multiple arguments, as shown below.
x = [1,2,3]
y = [4,5,6]
add(a,b) = a + b
result = map(add_two, x, y)
result
x = [1,2,3]
y = [4,5,6]
result = map((a,b) -> a + b, x, y)
result
x = [1,2,3]
y = [4,5,6]
result = map(x,y) do a,b # not (a,b)
a + b
end
result
To conclude this section, we cover how to document functions. This can be done by adding a string expression immediately before the function definition. Once this is done, the documentation can be accessed in the same manner as with built-in functions: you can type the function's name in the REPL after pressing ?, or directly hover over the function's name in VS Code. [note] Here, we explained how to access a function's documentation, under the subtitle "To See The Documentation of a Function".
"This function is written in a standard way. It takes a number and adds two to it."
function add_two(a)
a + 2
end
"This function is written in a compact form. It takes a number and adds three to it."
add_three(a) = a + 3
For further details, see the official documentation.