Functions
Mix.install([
{:kino, github: "livebook-dev/kino", override: true},
{:kino_lab, "~> 0.1.0-dev", github: "jonatanklosko/kino_lab"},
{:vega_lite, "~> 0.1.4"},
{:kino_vega_lite, "~> 0.1.1"},
{:benchee, "~> 0.1"},
{:ecto, "~> 3.7"},
{:math, "~> 0.7.0"},
{:faker, "~> 0.17.0"},
{:utils, path: "#{__DIR__}/../utils"}
])
Navigation
Setup
Ensure you type the ea
keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.
Functions
Elixir is a functional programming language. So you can imagine that functions must be important. But what is a function?
Input and Output (IO)
A function is a set of repeatable instructions. A function accepts some input, and returns some output.
flowchart LR
Input --> Output
Black Box
How the function converts some input to some output is often referred to as a black box. It’s a black box because you don’t need to know (or can’t know) the details of how it works.
flowchart LR
Input --> B[Black Box] --> Output
Creating A Function
Let’s create a function called double
which will take in a number and
double its value.
flowchart LR
A[2] --> B[double] --> C[4]
Now, let’s create our first function. At first, it’s going to do nothing.
A function must have an output. We can return nil
for now.
double = fn -> nil end
You may see some weird-looking output like #Function<45.65746770/0 in :erl_eval.expr/5>
.
Don’t worry too much about that. It’s how Elixir represents a function internally.
Parts of A Function
Let’s break down what we did above.
-
double
is a variable name. Often you’ll refer to it as the function name. It can be any valid variable name. -
We bind
double
to an anonymous function. The anonymous function is everything from thefn
to theend
.flowchart LR A[function name] --> B[=] B --> C[anonymous function]
-
Elixir uses the
fn
keyword to define a function. -
The next value
->
separates the function head and the function body. -
The function head describes the input of the function. In this example, it’s empty.
-
The function body contains the function’s implementation or black box . In this example, it returns
nil
. -
Elixir uses the
end
keyword to stop creating a function.
flowchart LR
direction LR
a[function name] --> B
b[function head] --> A
b[function head] --> B
c[function body] --> C
subgraph a[Breaking Down A Function]
direction LR
A[fn] ---- B
B[->] --- C
C[nil] --- D
D[end]
end
Calling A Function
Our double
function doesn’t do much at this point, but let’s see the output that it returns.
We use the .()
syntax in Elixir to get the function’s output. We often say we are executing or calling a function.
double.()
double
should return nil
because that’s all we’ve told it to do so far. However, we want
it to multiply a number by 2
.
To do that, we need to make the function accept some input. To do this, we define a parameter in the function like so.
double = fn parameter -> nil end
You’ll notice a warning above. That’s because Elixir is smart and lets us know that we’ve created a parameter, but we’re not using it.
In Elixir, you can ignore this warning for unused variables by starting them with an underscore _
double = fn _parameter -> nil end
No more warning 😀 But we actually want to use that parameter, so let’s modify the function to return the parameter instead.
double = fn parameter -> parameter end
The parameter is named parameter
here for the sake of example. But it works a lot like a variable,
and it can be named any valid variable name.
Let’s rename it to number
to clarify that we expect the input to be a number.
double = fn number -> number end
Now the function head takes in a value. We have to pass it
an argument when we call it. The argument will be bound to the parameter when the function
executes. We’ll give it the integer 2
.
double.(2)
Notice that if you try to call the function without an argument, it fails because it expects an argument. Not all languages do that, but Elixir is pretty smart 😎
double.()
Great, now all that’s left is to multiply the parameter by 2
. You should be familiar with
this from the previous sections.
double = fn number -> number * 2 end
And you can use it to double any number.
double.(10)
double.(11)
double.(10 ** 2 - 1)
Under the hood, when the function runs, the parameter is bound to the argument’s value.
Let’s break down how a function executes step by step in the following slideshow.
Utils.slide(:functions)
As expected, double.(3)
returns 6
.
double.(3)
Implied Return Values
Some languages require explicit return values.
However, in Elixir the output of a function is always the last line.
For example, notice that the return value below is first
+ second
, which equals 3
.
multiline_function = fn ->
first = 1
second = 2
first + second
end
multiline_function.()
Your Turn
:walnuts
In the Elixir cell below, create a function is_even?
that checks if an integer is even or not.
The function should return true
if the integer is even and false
if the integer is odd.
Replace nil
with your anonymous function.
Hint
You can use rem
to determine if an integer is even with rem(integer, 2) == 0
.
is_even? = nil
Utils.feedback(:is_even?, is_even?)
Multi-Parameter Functions
Functions can accept multiple inputs. Separate parameters with commas ,
to create a multi-parameter function.
sum3 = fn param1, param2, param3 -> param1 + param2 + param3 end
sum3.(2, 3, 4)
Keep in mind that the first argument will be the value of the first parameter, and the second argument will be the value of the second parameter. You can repeat this with as many parameters as you want!
to_list = fn a, b, c, d, e -> [a, b, c, d, e] end
to_list.(1, 2, 3, 4, 5)
But usually, you want to avoid having too many parameters because it makes your function hard to understand.
A parameter can be bound to any valid data type, so you could instead use an associative data structure like a map or keyword list.
Function Arity
The number of parameters your function accepts is called the arity of the function.
A function with no parameters has an arity of zero. A function with one parameter has an arity of one, and so on.
You refer to the function as function_name/arity
thus a function named add_two
with two parameters
is called add_two/2
.
Your Turn
Create a multi-parameter calculate_force/2
function.
calculate_force/2
should accept mass
and acceleration
and return mass * acceleration
.
Replace nil
with your anonymous function.
calculate_force = nil
Utils.feedback(:calculate_force, calculate_force)
Shorthand Syntax
Anonymous functions can be defined using a shorthand syntax. It is only an alternative and shorter version to define a function. You will sometimes see shorthand syntax, so it’s helpful to understand it. However, it should not be over-used. Otherwise, your program may be less clear.
You can still bind the anonymous function to a variable with the shorthand syntax.
However, you define the function with &()
and put the function body between the brackets.
Here’s the same double
function using short-hand syntax.
double = &(&1 * 2)
double.(5)
&1
means the first parameter. If the function had more parameters, you could access them with &2
, &3
, and so on.
add_two = &(&1 + &2)
add_two.(2, 3)
Your Turn
Using shorthand syntax, create a calculate_force
function which multiplies mass
and acceleration
.
Replace nil
with your answer.
calculate_force = nil
Utils.feedback(:calculate_force, calculate_force)
First-class Functions
Functions in Elixir are first-class citizens.
For our purposes, this means we can bind functions to variables, store them in other data types, pass them as arguments to other functions.
If a function takes in another function as a parameter, it’s called a higher-order function.
For example, we could make a function named call_twice
, which calls a function on a value twice.
call_twice = fn function, parameter -> function.(function.(parameter)) end
We’ll use our double
function and create a quadruple result.
double = fn number -> number * 2 end
call_twice.(double, 3)
We don’t need to bind the passed function to a variable first. We can pass in the anonymous function directly.
call_twice.(fn number -> number * 2 end, 5)
Whenever you pass a function into a higher-order function with the expectation that the higher-order function is going to call it, the passed function is referred to as a callback function.
Your Turn
Create a call_with_20
function. the call_with_20
function should accept a function as it’s
argument and call that function with the integer 20
.
Replace nil
with your anonymous function.
call_with_20 = nil
Utils.feedback(:call_with_20, call_with_20)
Pipe Operator
To create more complex behavior, you’ll often compose smaller functions together. Composing functions together reflects nature of problem-solving where we take large problems and break them down into smaller ones.
To help compose functions together, Elixir provides the pipe |>
operator.
That’s the |
symbol likely above your enter key, and the greater than >
symbol side by side to make |>
.
The pipe operator allows you to take the output of one function and pass it in as an argument for the input of another function.
flowchart LR
A[Input] --> B[Function1]
B --> C[Pipe]
C --> D[Function2]
D --> E[Output]
Why is this useful? Without the pipe operator you can wind up writing deeply nested function calls.
four.(three.(two.(one.())))
Or rebinding values between function calls.
a = one.()
b = two.(a)
c = three.(b)
d = four.(c)
But with the pipe operator, you can chain functions together.
one.() |> two.() |> three.() |> four.()
If a function is called with multiple arguments, the function piped in will be the first argument.
two(1, 2) # how to call two/2 by itself.
# How to use the pipe operator
# to call the two/2 with one/1 as the first argument.
one.() |> two.(2)
You can also pass in a value to a pipe. It’s generally non-idiomatic to use the pipe operator for a single value and function.
# non-idiomatic
1 |> two.()
# idiomatic
1 |> two.() |> three()
The pipe operator doesn’t change the behavior of a program. Instead, the pipe operator exists as syntax sugar to improve the clarity of your code.
Your Turn
Use the add/2
, subtract/2
, and multiply/2
functions provided.
Use the pipe operator to:
-
start with
10
. -
add
2
. -
multiply by
5
. -
subtract by
4
.
Replace nil
with your answer.
add = fn int1, int2 -> int1 + int2 end
multiply = fn int1, int2 -> int1 * int2 end
subtract = fn int1, int2 -> int1 - int2 end
answer = nil
Utils.feedback(:pipe_operator, answer)
Commit Your Progress
Run the following in your command line from the project folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish functions section"