Functions without Arguments
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.
<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).
When writing a script, you'll need to define your own functions. 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. Conversely, 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 returns the last line as its output. You can alternatively specify the output to return through the keyword return
. You can also return multiple outputs by defining a collection as the function's result. For this purpose, tuples are the most common choice. [note] This is in fact because tuples are more performant than vectors, as long as 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)
. The distinctive feature of this syntax is that arguments are passed in a specific order. Thus, foo(1,2)
assigns the first argument to x
and the second to y
. Function arguments of this kind are known as positional arguments.
One major drawback of positional arguments is their susceptibility to silent errors: if we accidentally swap the positions of arguments, the function may still provide an output. As the number of arguments grows, the likelihood of introducing bugs increases, making it more challenging to detect and debug errors.
One way to circumvent this issue is to use keyword arguments. They require function calls to explicitly specify their arguments using a syntax like foo(x=2,y=4)
. In this case, the order of arguments also becomes irrelevant, rendering foo(x=2,y=4)
and foo(y=4,x=2)
valid and equivalent.
The following examples illustrate how to define and call functions using positional and keyword arguments. Additionally, we show that the approaches can be combined. Note that positional arguments necessarily require a semicolon for its definition, but accept a semicolon or a comma during 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
An additional advantage of keyword arguments is that they allow us to set default values for their arguments.
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 the user to define subsequent arguments in terms of previous ones. For example, given foo(;x,y)
, the default value of y
could be 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)
, it's possible to 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 represent a third way to define functions. Unlike the previous definitions of functions, they're commonly introduced with a different purpose: 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, anonymous functions that depend on two or more arguments are expressed by (x,y) -> <body of the function>
.
For the demonstration, let's consider the built-in function map(<function>, <collection>)
. This function takes another function as its first argument, which is then applied element-wise to the collection passed as the second argument. 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]
. In subsequent sections, we'll explain the function map
in detail. For now, just take it as an example for anonymous functions.
The execution of map(add_two, x)
requires defining add_two
previous to calling it. The creation of temporary function not only clutters our workspace, but it's additionally inconvenient if add_two
won't be reused. In this context, an anonymous function represents an elegant solution, eliminating the need to create 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
By using map
, we can also demonstrate the syntax of anonymous functions having multiple arguments. For operations requiring more than one argument, its syntax is 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 be useful to keep our code tidy. However, they aren't particularly practical if functions span over several lines. Nonetheless, the inconvenience can be mitigated by what's known as a do-block. This allows us to insert the anonymous function separately, and then pass it as the first argument to a function call. Its syntax for a function foo(<inner function>, <vector>)
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 user-defined functions. This involves adding a string expression immediately before the function definition.
After this, 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.