Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
{:smart_animation, github: "brooklinjazz/smart_animation"}
])
Upon completing this lesson, a student should be able to answer the following questions.
- How do we define a function?
- How do we call a function?
- How do we use the pipe operator
|>
to pipe function calls into each other?
Elixir is a functional programming language. So you can imagine that functions must be important. But what is a function?
A function is a set of repeatable instructions. A function accepts some input, and returns some output.
flowchart LR
Input --> Output
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
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.
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
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.
frames = [
"
First, we define the `double` function and call it.
```elixir
double = fn number -> number * 2 end
double.(3)
```
",
"
The `double` function executes in place of the `double.(call)` with `number` bound to `3`.
```elixir
double = fn number -> number * 2 end
fn 3 -> 3 * 2 end
```
",
"
The function evaluates the function body between the `->` and the `end`
```elixir
double = fn number -> number * 2 end
3 * 2
```
",
"
`3` * `2` is `6`, so the function call returns `6`.
```elixir
double = fn number -> number * 2 end
6
```
"
]
SmartAnimation.new(0..(Enum.count(frames) - 1), fn i ->
Enum.at(frames, i) |> Kino.Markdown.new()
end)
As expected, double.(3)
returns 6
.
double.(3)
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.()
Create a function is_even?/1
that accepts an integer, and returns true
if the integer is even, or false
if the integer is odd.
is_even?.(2) # true
is_even?.(1) # false
Example solution
is_even? = fn int -> rem(int, 2) == 0 end
Enter your solution below.
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.
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
.
Create a function calculate_force/2
that accepts a mass
and acceleration
parameters.
The calculate_force/2
function should return mass * acceleration
.
calculate_force.(10, 10) # 100
calculate_force.(2, 4) # 8
Example solution
calculate_force = fn mass, acceleration -> mass * acceleration end
Enter your solution below.
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)
Using shorthand syntax, create a subtract/2
function which subtracts two numbers.
subtract.(1, 1) # 0
subtract.(20, 25) # -5
Example solution
subtract = &(&1 - &2)
Enter your solution below.
Using shorthand syntax, create a multiply_three/3
function which multiplies three numbers together.
multiply_three.(2, 2, 2) # 8
multiply_three.(2, 5, 3) # 30
Example solution
multiply_three = &(&1 * &2 * &3)
Enter your solution below.
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.
We'll see something like #Function<42.3316493/1 in :erl_eval.expr/6>
if we try to view a function as data. This is the internal representation of the function in Elixir. We can only view a function this way, we cannot use this syntax in our code.
[fn int -> int * 2 end, 2, 3, 4]
Functions are just data as far as our programs are concerned. We can work with them like we would work with any data structure.
[my_function | _tail] = [fn int -> int * 2 end, 2, 3, 4]
my_function.(10)
If a function takes in another function as a parameter, it's called a higher-order function. The function passed in is called a callback function.
callback_function = fn -> end
higher_order_function.(callback_function)
For example, we can create a call_with_2/1
higher order function, that accepts a callback function, and calls it with the integer 2
.
call_with_2 = fn callback -> callback.(2) end
We can then use the call_with_2/1
higher order function with an add_three/1
callback function. This calls add_three/1
with 2
to return 5
.
add_three = fn int -> int + 3 end
call_with_2.(add_three)
Instead of binding the callback function to a variable, we can pass in the anonymous function directly.
call_with_2.(fn int -> int + 3 end)
Callback functions are useful for creating re-usable behavior with slight alterations. For example, in Elixir we have a built-in function Enum.map/2 which we will cover in later lessons.
Enum is a module. We'll cover modules in the next Modules reading material.
Enum.map/2 can accept a list as the first argument, and a callback function as the second argument. The callback function is applied to every element in the list to create a new list.
Enum.map([1, 2, 3], fn element -> element * 2 end)
Create a call_with_10_and_20/1
function. The call_with_10_and_20/1
function should accept a call back function, and call the function with 10
as the first argument, and 20
as the second argument.
add = fn int1, int2 -> end
call_with_10_and_20.(add) # 30
Example solution
call_with_10_and_20 = fn callback -> callback.(10, 20) end
Enter your solution below.
call_with_10_and_20 = nil
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 (not following the standards of the community) to use the pipe operator for a single value and function.
# Non-idiomatic
1 |> two.()
# Idiomatic
two.(1)
# 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.
Create three functions add/2
, subtract/2
, and multiply/2
which add, subtract, or multiply two numbers.
Use these functions with the pipe operator |> to:
- start with
10
. - add
2
. - multiply by
5
. - subtract by
4
.
Example solution
add = fn int1, int2 -> int1 + int2 end
subtract = fn int1, int2 -> int1 - int2 end
multiply = fn int1, int2 -> int1 * int2 end
10
|> add.(2)
|> multiply.(2)
|> subtract.(4)
Enter your solution below.
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add .
$ git commit -m "finish Functions reading"
$ git push
We're proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.