Functions

Author

Marie-Hélène Burle

Functions are objects containing a set of instructions.
When you pass a tuple of argument(s) (possibly an empty tuple) to them, you get one or more values as output.

Operators

Operators are functions and can be written in a way that shows the tuple of arguments more explicitly.

For instance, you can use the addition operator (+) in 2 ways:

3 + 2
+(3, 2)
5

The multiplication operator can be omitted when this does not create any ambiguity:

a = 3;
2a
6

Julia has “assignment by operation” operators:

a = 2;
a += 7    # this is the same as a = a + 7
9

There is a left division operator:

2\8 == 8/2
true

Julia supports fraction operations:

4//8
1//2
1//2 + 3//4
5//4

Function definition

There are 2 ways to define a new function:

Long form

function <name>(<arguments>)
    <body>
end

Example:

function hello1()
    println("Hello")
end
hello1 (generic function with 1 method)

Assignment form

<name>(<arguments>) = <body>

Example:

hello1() = println("Hello")
hello1 (generic function with 1 method)

The function hello1 defined with this terse syntax is exactly the same as the one we defined above.

Stylistic convention

Julia suggests to use lower case without underscores as function names when the name is readable enough.

Calling functions

Since you pass a tuple to a function when you run it, you call a function by appending parentheses to its name:

hello1()
Hello

Here, our function does not take any argument, so the tuple is empty.

Arguments

No argument

Our function hello1 does not accept any argument. If we pass an argument, we get an error message:

hello1("Bob")
LoadError: MethodError: no method matching hello1(::String)

One argument

To define a function which accepts an argument, we need to add a placeholder for it in the function definition.

So let’s try this:

function hello2(name)
    println("Hello name")
end
hello2 (generic function with 1 method)
hello2("Bob")
Hello name

Mmm … not quite … this function works but does not give the result we wanted.

Here, we need to use string interpolation:

function hello3(name)
    println("Hello $name")
end
hello3 (generic function with 1 method)

$name in the body of the function points to name in the tuple of argument.

When we run the function, $name is replaced by the value we used in lieu of name in the function definition:

hello3("Bob")
Hello Bob

Here is the corresponding assignment form for hello3:

hello3(name) = println("Hello $name")
hello3 (generic function with 1 method)

Note that this dollar sign is only required with strings. Here is an example with integers:

function addTwo(a)
    a + 2
end
addTwo (generic function with 1 method)

And the corresponding assignment form:

addTwo(a) = a + 2
addTwo (generic function with 1 method)
addTwo(4)
6

Multiple arguments

Now, let’s write a function which accepts 2 arguments. For this, we put 2 placeholders in the tuple passed to the function in the function definition:

function hello4(name1, name2)
    println("Hello $name1 and $name2")
end
hello4 (generic function with 1 method)

This means that this function expects a tuple of 2 values:

hello4("Bob", "Pete")
Hello Bob and Pete

Your turn:

See what happens when you pass no argument, a single argument, or three arguments to this function.

Default arguments

You can set a default value for some or all arguments. In this case, the function will run with or without a value passed for those arguments. If no value is given, the default is used. If a value is given, it will replace the default.

Example:

function hello5(name="")
    println("Hello $name")
end
hello5 (generic function with 2 methods)
hello5()
Hello 
hello5("Bob")
Hello Bob

Another example:

function addSomethingOrTwo(a, b=2)
    a + b
end
addSomethingOrTwo (generic function with 2 methods)
addSomethingOrTwo(3)
5
addSomethingOrTwo(3, 4)
7

Returning the result

In Julia, functions return the value(s) of the last expression automatically.
If you want to return something else instead, you need to use the return statement. This causes the function to exit early.

Look at these 5 functions:

function test1(x, y)
    x + y
end

function test2(x, y)
    return x + y
end

function test3(x, y)
    x * y
    x + y
end

function test4(x, y)
    return x * y
    x + y
end

function test5(x, y)
    return x * y
    return x + y
end

function test6(x, y)
    x * y, x + y
end

Your turn:

Without running the code, try to guess the outputs of:

test1(1, 2)
test2(1, 2)
test3(1, 2)
test4(1, 2)
test5(1, 2)
test6(1, 2)

Your turn:

Now, run the code and draw some conclusions on the behaviour of the return statement.

Anonymous functions

Anonymous functions are functions which aren’t given a name:

function (<arguments>)
    <body>
end

In compact form:

<arguments> -> <body>

Example:

function (name)
    println("Hello $name")
end
#13 (generic function with 1 method)

Compact form:

name -> println("Hello $name")
#15 (generic function with 1 method)

When would you want to use anonymous functions?

This is very useful for functional programming (when you apply a function—for instance map—to other functions to apply them in a vectorized manner which avoids repetitions).

Example:

map(name -> println("Hello $name"), ["Bob", "Lucie", "Sophie"]);
Hello Bob
Hello Lucie
Hello Sophie

Pipes

|> is the pipe in Julia.
It redirects the output of the expression on the left as the input of the expression on the right.

The following 2 expressions are equivalent:

println("Hello")
"Hello" |> println

Here is another example:

sqrt(2) == 2 |> sqrt
true

Function composition

You can pass a function inside another function:

<function2>(<function1>(<arguments>))

<arguments> will be passed to <function1> and the result will then be passed to <function2>.

An equivalent syntax is to use the composition operator (in the REPL, type \circ then press tab):

(<function2> ∘ <function1>)(<arguments>)

Example:

# sum is our first function
sum(1:3)
6
# sqrt is the second function
sqrt(sum(1:3))
2.449489742783178
# This is equivalent
(sqrt  sum)(1:3)
2.449489742783178

Your turn:

Write three other equivalent expressions using the pipe.

Another example:

exp(+(-3, 1))

(exp  +)(-3, 1)
0.1353352832366127

Your turn:

Try to write the same expression in another 2 different ways.

Mutating functions

Functions usually do not modify their argument(s):

a = [-2, 3, -5]
3-element Vector{Int64}:
 -2
  3
 -5
sort(a)
3-element Vector{Int64}:
 -5
 -2
  3
a
3-element Vector{Int64}:
 -2
  3
 -5

Julia has a set of functions which modify their argument(s). By convention, their names end with !

The function sort has a mutating equivalent sort!:

sort!(a);
a
3-element Vector{Int64}:
 -5
 -2
  3

If you write functions which modify their arguments, make sure to follow this convention too.

Broadcasting

To apply a function to each element of a collection rather than to the collection as a whole, Julia uses broadcasting.

Let’s create a collection (here a tuple):

a = (2, 3)
(2, 3)

If we pass a to the string function, that function applies to the whole collection:

string(a)
"(2, 3)"

In contrast, we can broadcast the function string to all elements of a:

broadcast(string, a)
("2", "3")

An alternative syntax is to add a period after the function name:

string.(a)
("2", "3")

Here is another example:

a = [-3, 2, -5]
abs(a)
ERROR: MethodError: no method matching abs(::Array{Int64,1})

This doesn’t work because the function abs only applies to single elements.

By broadcasting abs, you apply it to each element of a:

broadcast(abs, a)
(2, 3)

The dot notation is equivalent:

abs.(a)
(2, 3)

It can also be applied to the pipe, to unary and binary operators, etc.

Example:

a .|> abs
(2, 3)

Your turn:

Try to understand the difference between the following 2 expressions:

abs.(a) == a .|> abs
abs.(a) .== a .|> abs
(true, true)

Multiple dispatch

In some programming languages, functions can be polymorphic (multiple versions exist under the same function name). The process of selecting which version to use is called dispatch.

There are multiple types of dispatch depending on the language:

  • Dynamic dispatch: the process of selecting one version of a function at run time.
  • Single dispatch: the choice of version is based on a single object.

This is typical of object-oriented languages such as Python, C++, Java, Smalltalk, etc.

  • Multiple dispatch: the choice of version is based on the combination of all operands and their types.

This the case of Lisp and Julia. In Julia, these versions are called methods.

Methods

Running methods(+) let’s you see that the function + has 206 methods!

Methods can be added to existing functions.

Your turn:

Run the following and try to understand the outputs:

abssum(x::Int64, y::Int64) = abs(x + y)
abssum(x::Float64, y::Float64) = abs(x + y)

abssum(2, 4)
abssum(2.0, 4.0)
abssum(2, 4.0)

What could you do if you wanted the last expression to work?