You might notice that tuples and lists seem very similar, and they are. They are both data types that contain a collection of elements.
The major difference is performance. Both are good at different things. In Elixir, it's fast to retrieve an element from a tuple, however it's slow to add a new element in.
Why? Well, that requires some understanding of how memory works in a computer. The short version is that a tuple is stored continuously all together. So, you know where each element is on the computer.
flowchart
1[location 1] --- T
2[location 2] --- U
3[location 3] --- P
4[location 4] --- L
5[location 5] --- E
However, if you want to add a new element in that means you have to move the location of every element.
Lists on the other hand have the opposite strengths and weaknesses. Lists are stored as linked lists which means each element in the list knows the location of the next element.
flowchart LR
L -- location of I --> I -- location of S --> S -- location of T --> T
So we can store elements in a list anywhere, if we need to add or remove a new element we only have to change the location one element is pointing to.
However, we don't know the location of every element upfront like with a tuple, so if you want to get the third element, you need to look through the first and second to find it.
Comparison operators are not limited to comparing integers and floats. For example, they are often used to compare the equality of other data types.
"hello" === "hello"
%{hello: "world"} === %{hello: "world"}
["hello", "world"] === ["hello", "world"]
Programming languages hide the internal details of how computers work. This allows us as developers to think at a high level. However, it's still useful to be aware of some of the inner workings.
Under the hood, computers store electrical signals. You can think of this like powering a lightbulb but on a much smaller scale. An electrical signal is either on or off, and by manipulating these electrical signals, we are able to create complex information and instruction.
You may have seen movies or tv series that show coding as a bunch of 1
s and 0
s
. A 0 represents a signal that's off and a
1
represents a signal
that's on. These 0
s and 1
s are called binary.
After we write our Elixir code, our instructions are then converted or compiled into binary. binary is the machine code that the computer actually understands.
So, you might wonder how we go from 0
s and 1
s to creating rockets, websites, self-driving
cars, smartphones, and 3D animations.
Well let's take the first step with booleans. A boolean (BOO-LEE-AN).
is a true
or a false
value. Conceptually, this is a direct representation of
on
and off
in your program.
There are only two booleans.
- Pattern matching with the match operator
- Using
with
to check a series of conditions before entering a path. - using Multi-clause functions with pattern matching
- Handling errors with the :ok, and :error tuple and pattern matching
Which means you can also use pattern matching on the elements in a map.
Enum.map(%{key: "value"}, fn {_key, value} -> value end)
Map keys can still be any type in the tuple.
Enum.map(%{%{} => "value"}, fn {key, _value} -> key end)
You can use pattern matching inside of the function.
Enum.map([one: 1], fn {_key, value} -> value end)
Instead of specific constructs that enable polymorphism,
you can instead use control flow constructs like if
, cond
, and case
to
alter the behavior of your programs.
Often though, we use polymorphism to improve the clarity of your code where if
, cond
, and case
would become less clear. For example, let's say we don't use polymorphism with multiple arity functions
and instead write a single function with a list for the parameter.
We'd need to check the length of names with length/1
,
and retrieve each element in the list.
defmodule Greeter do
def hi(names) do
case length(names) do
1 -> "Hi #{Enum.at(names, 0)}!"
2 -> "Hi #{Enum.at(names, 0)} and #{Enum.at(names, 1)}!"
_ -> "Hi everyone!"
end
end
Is it better or worse? It's not always clear-cut, but it's useful to rely on polymorphism when it would improve the clarity of your code.
Ensure that you evaluate all code using the ea
keybinding. Press the e
then the a
key.
Mix.install([{:kino, github: "livebook-dev/kino"}])
You can think of all programming as split into data and behavior. In the previous lesson, you learned all about data, and in this lesson, you're going to learn about behavior.
To operate means to control a machine, process, or system. Thus we have operators which control the behavior of our program.
In this lesson, you're going to learn:
- Arithmetic operators performing mathematical operations.
+
,-
,*
,/
,div
, andrem
- Match operator binding data to a variable.
=
- Comparison operators comparing values to one another.
===
==
>=
<=
<
>
- Boolean operators comparing booleans and truthy values.
and
or
not
&&
||
!
- String operators manipulating strings.
<>
,#{}
- List operators manipulating lists.
++
--
- Accessing map values with map.key notation and map[key] notation.
So far, you've only used operators in isolation. But it's important to remember that you can use them in combination.
2 + 3 > 4
"4 + 7 is greater than 8 and 2 + 10 is less than 20: #{4 + 7 > 8 && 2 + 10 < 20}"
Here's a small program that checks if a variable is between two integers.
You'll notice that code can include line breaks to make it easier to read.
Try setting input
to 15
to see it return. "15 is between 10 and 20"
.
input = 5
max = 20
min = 10
(input < max && input > min &&
"#{input} is between #{min} and #{max}") || "#{input} is outside #{min} and #{max}"
In the Elixir cell below, You're going to create the perfect AI for rock paper scissors.
You should only need to use &&
, =
, ===
, ||
operators.
- Create a variable
input
and bind it to"Rock"
. - Use comparison operators to check if input equals "Rock" then return "Paper"
- Bonus: handle if input equals "Paper" then return "Scissors"
- Bonus: handle if input equals "Scissors" then return "Rock"
Syntax
refers to the keywords and symbols you use to provide instruction to the computer through
Elixir. If you do not follow the rules that Elixir sets, your code will not
compile into instructions for the computer, and you will see a red error
message.
Sometimes your code is valid, but you may still see a yellow warning
. Warnings let you know
that you might be making a mistake in your program, but the code still compiles into valid
instruction.
In general, in programming, you have to be precise. A single misplaced character causes your entire program to stop working!
Take care to regularly evaluate any code you're writing to ensure it compiles. Regularly verifying your code compiles makes it a lot easier to know if a change triggered an error or warning.
Elixir does its best to let you know the cause of your error, but it can take some time to get used to reading these messages.
For example, here's the error messages you receive when you have a dangling operator without a value on the right-hand side.
2 *
In general, errors will provide you an error type, error message, line number, and even the code causing the crash
Here's what the error above tells us.
- Error type: TokenMissingError because we're mising the next token
(in this case a number) in the
2 * number
expression. - Error message:
syntax error: expression is incomplete
- Code causing the crash:
| 1 | 2 * | ^
- Line number: the crash is on line 1
In the Elixir cell below, Reproduce a crash by writing "hello" <>
.
Then, try using Enter
to create a new line and shift the code down to line 2. See how the
error is different now.
require Integer
You're about to unleash the full power and potential of programming.
Data structures and operators are fundamental for providing instructions to the computer, but you're limited in how repeatable and reusable those instructions can be.
For example, if you want to determine if many numbers are even, you must repeatedly reimplement that behavior.
Reusing repeatable instructions makes programming far more powerful. This is because each solved problem acts as a building block for the next.
To abstract in our context means to reduce complexity by hiding implementation details.
You work with abstractions all the time. For example, it was mechanical and complex when you first learned to walk. Now you move without even thinking about it.
Any machine you operate is an abstraction. Do you drive? You've probably never needed to think about the complex machinery triggered by turning a wheel. How about typing? That's right. Your keyboard is an abstraction for the complex electrical signals required to communicate with your computer.
Human beings have limited brains, so we deal with complexity by creating abstractions.
In programming, we rely on abstractions in much the same way. We figure out how to solve small problems, and then we reuse those small abstractions to solve more significant challenges.
flowchart LR
M1 --- M2
subgraph Large Problem
subgraph M1[Medium Problem]
direction LR
A[small problem] --- B[small problem]
B --- C[small problem]
end
subgraph M2[Medium Problem]
direction LR
D[small problem] --- E[small problem]
E --- F[small problem]
end
end
Modules and functions are the primary tools we have to create abstractions and repeat behavior.
In total this lesson will cover:
- How to abstract away implementation details with functions.
- How to create multi-parameter functions accept multiple inputs.
- An alternative shorthand syntax for functions.
- How we can use first-class functions as values in higher-order functions
- How to group functions in a module.
- Private and public functions in a module.
- Using module attributes to create reusable constant valuse between module functions.
- How to achieve polymorphism with multiple function clauses.
- How to compose functions together using the pipe operator.
- Validating function input with guards.
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]
B --> Output
Let's create a function called double
which will take in a number and
double its value.
flowchart LR
A[Input `2`] --> B[BlackBox `double`]
B --> C[Output `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, if you want to have an unused variable
without warning, you can mark it 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 anything.
Let's rename it to number
to clarify what we expect the input to the function to be.
double = fn number -> number end
Now the function head takes in a value. We have to pass it
an argumentwhen we call it. The argument is binded to the parameter when the function
executes. We'll give it the integer 2
.
double.(2)
Under the hood, when the function runs, the parameter is binded to the argument's value.
Step by step, that would look like this.
double = fn number -> number end
double.(2)
fn 2 -> 2 end
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 😎
is_even.()
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)
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.()
Functions can accept multiple inputs. Separate parameters with commas ,
to create a multi-parameter function.
Let's say you're creating a function calculate_force
, which will take in mass
and
acceleration
. (remember that force = mass * acceleration)
Let's start with the double
function and see how we could convert it into a calculate_force
function.
double = fn number -> number * 2 end
First, let's rename double
to calculate_force
and rename number
to mass
. That gets us
most of the way there!
calculate_force = fn mass -> mass * 2 end
Technically, we've got a function that calculates the force of an object with a given mass and an acceleration of 2.
However, we want it to be truly reusable, so we need to figure out how to replace 2 with another
parameter. You can declare a function that accepts multiple parameters by separating them with a
comma ,
as you do in a list or tuple.
calculate_force = fn mass, acceleration -> mass * acceleration end
That should be working! Now you can call the function by passing in two arguments the same way.
calculate_force.(10, 2)
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!
example = fn a, b, c, d, e -> [a, b, c, d, e] end
example.(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 associative data a structure like a map or keyword list.
is_even = nil
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)
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
.
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.
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.())))
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.
1 |> two.()
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.
add_grapes = fn shopping_list -> shopping_list ++ ["grapes"] end
add_pizza = fn shopping_list -> shopping_list ++ ["pizza"] end
add_blueberries = fn shopping_list -> shopping_list ++ ["blueberries"] end
[]
|> add_grapes.()
|> add_pizza.()
|> add_blueberries.()
|> add_pizza.()
|> add_blueberries.()
# vs the alternative
add_blueberries.(add_pizza.(add_blueberries.(add_pizza.(add_grapes.([])))))
As you create more and more functions, it becomes necessary to organize them. That's just one of the many reasons to use a module. A module is more or less a "bag of functions".
Here's what an empty module looks like.
defmodule Hello do
end
Again, don't worry about the somewhat strange-looking return value.
{:module, Hello, <<70, 79, 82, 49, 0, 0, 4, ...>>, nil}
That's how Elixir represents
modules internally.
Let's break down what this all means.
defmodule
a keyword that means "define module".Greeter
is the name of this module. It can be any valid name, and is usually CapitalCase. you'll often heard the name of the module refered to as the namespace that functions are organized under.do
a keyword that separates the module name and its internal implementation.end
a keyword that finishes the module definition.
Modules define functions inside of them. Each function has a name, so they are called named functions. You can define functions inside a module using the following syntax.
defmodule Greeter do
def hello do
"hello world"
end
end
Let's break down the named function above.
def
this means "define function"do
a keyword that separates the function head and the function body."hello world"
this is the function body. This function returns the string "hello world"end
is a keyword that ends the function definition.
To call a function inside a module, you use Module.function(args)
syntax.
Greeter.hello()
You can create multiple functions in a module.
Here's a new hi/1
function that says hi to a person.
defmodule Greeter do
def hello do
"hello world"
end
def hi(name) do
"hi #{name}"
end
end
You call the named function in the module by passing it an argument.
Greeter.hi("Brooklin")
A module can use its own functions.
defmodule InspectorGadget do
def gogo(gadget) do
"Go go gadget #{gadget}!"
end
def necktie do
InspectorGadget.gogo("Necktie")
end
end
InspectorGadget.necktie()
However, you can omit the module name. notice gogo
instead of InspectorGadget.gogo
defmodule InspectorGadget do
def gogo(gadget) do
"Go go gadget #{gadget}!"
end
def necktie do
gogo("Necktie")
end
end
InspectorGadget.necktie()
Modules can access other module functions.
defmodule Speaker do
def speak() do
"hi there"
end
end
defmodule Listener do
def listen() do
"I heard you say: " <> Speaker.speak()
end
end
Listener.listen()
However, sometimes a module must keep a function private for internal use only. Why? It may be for security reasons or because you don't think it's unnecessary to expose the function to the outside world. Often it communicates to other developers how to use your module.
You can create a private module function with defp
instead of def
. You'll notice that below the
Speaker.think/0
function is undefined to the outside world.
defmodule Speaker do
defp think() do
"hi there"
end
end
Speaker.think()
We use private functions internally in the module, which means that public functions could expose their values.
defmodule Speaker do
defp think() do
"hi there"
end
def speak() do
think()
end
end
Speaker.speak()
What if you have many functions in a module that all use the same value? You've already learned that repeating the same hard-coded value over and over again isn't very reusable, and you've used variables to pass the same value around in your code. However, that's not possible in a module.
Modules and functions in a module close themselves to the outside world. We call this scope
.
Modules, functions, and many other similar constructs in Elixir are `lexically scoped.
That means that variables defined in one scope cannot be accessed in another scope.
flowchart
subgraph Top Level Scope
A[top level variable]
subgraph Module Scope
B[module variable]
subgraph Function Scope
C[function variable]
end
end
end
Notice how the following example has an error because we cannot access the variable
top_level_scope
top_level_scope = 1
defmodule MyModule do
def my_function do
top_level_scope
end
end
The same is true for the module scope.
defmodule MyModule do
module_scope = 2
def my_function do
module_scope
end
end
To get around this, Elixir allows you to store constant values as module attributes
using the @module_attribute value
syntax. You can then access the @module_attribue
value in
any module function.
flowchart
direction TB
subgraph A[Top Level Scope]
a[Top Level Variable] --- b
direction TB
subgraph B[Module Level Scope]
b[Define Module Attribute] --- c
subgraph C[Function Level Scope]
c[Access Module Attribute]
end
end
end
Notice that we can use a module attribute inside of the module's function now.
defmodule MyModule do
@my_attribute "any valid data type"
def my_function do
@my_attribute
end
end
MyModule.my_function()
We can also access a variable in the top-level scope as long as we set a module attribute.
top_level_variable = 1
defmodule MyModule do
@my_attribute top_level_variable
def my_function do
@my_attribute
end
end
MyModule.my_function()
Now we can easily share constant values between multiple module functions.
defmodule Hero do
@name "Batman"
@nemesis "Joker"
def catchphrase do
"I am #{@name}."
end
def victory do
"I #{@name} will defeat you #{@nemesis}!"
end
end
Hero.victory()
If the module attribute value changes, you only need to change the module attribute.
defmodule Hero do
@name "IronMan"
@nemesis "Mandarin"
def catchphrase do
"I am #{@name}."
end
def victory do
"I #{@name} will defeat you #{@nemesis}!"
end
end
Hero.victory()
Elixir allows us to define multiple functions with the same name but that expect different parameters.
This means the function has multiple function clauses.
For example, we can take our hi
function and create a new version that says hi to two people.
defmodule Greeter do
def hi(name1, name2) do
"hi #{name1}, hi #{name2}"
end
def hi(name) do
"hi #{name}"
end
end
Greeter.hi("Peter Parker", "Mary Jane")
In the example above, each function has a different arity. You can treat each function with a different arity as a different function. We often refer to each function by its arity as function/arity.
So the Greeter
module has a hi/1
(hi one) function and hi/2
(hi two) function.
The word polymorphism comes from the Greek word for "many shapes". It means essentially the same thing for our programs. We often want to define a single general behavior, but apply it to different things.
In the example above, we use multiple function clauses to achieve polymophism using the same
generic hi
function to say hi to one person, or two people. Earlier, we also achieved polymorphism
with the call_twice
function, which abstracts a generic repeatable behavior, but does not
care about the specific function that it's calling twice.
flowchart
A[Polymorphic Behavior] --- B[Specific Implementation]
A --- C[Specific Implementation]
A --- D[Specific Implementation]
A --- E[Specific Implementation]
A --- F[Specific Implementation]
Running is a great real-world example of polymorphism. Many different animals run, and they all run in different ways.
flowchart
A[Run] --- B[Cheetahs]
A --- C[Cats]
A --- D[Dogs]
A --- E[Horses]
A --- F[Humans]
require Integer
Have you ever heard the expression "Reinventing the wheel?". Essentially it means repeating a useless action or solving an already solved problem.
In programming, it's important to avoid reinventing the wheel, and instead rely on previously existing solutions. Sometimes those previously existing solutions are our own!
In the previous lesson you learned about how to use modules and functions in order to abstract away generic and reusable behavior.
We also talked about polymorphism. Polymorphic code maintains consistent behavior while changing the underlaying implementation.
In this lesson, you'll learn about more constructs and built-in tools Elixir provides to enable reusable code and polymorphic code.
In this lesson we'll talk about:
- Custom reusable data structures with Structs.
- Reusable pre-built functionality with Built-In Elixir Modules.
- Polymorphic code with Behaviours.
- Polymorphic code with Protocols.
Under the hood, the values bound to variables are stored on the computer.
The variable, is actually pointing to the value in that location.
flowchart LR
variable --> location
For example, a variable hello
with the value "world"
would store the string
"world"
somewhere in memory. memory is a hardware component on the computer.
You may have heard of RAM (Random Access Memory).
flowchart LR
hello --> w["world"]
Object Oriented Programming languages allow you to mutate the actual value in memory.
So when we write hello = 4
, the data "world"
mutates into 4
.
flowchart LR
hello --> w["world"]
However, in a functional programming language like Elixir, we do not allow mutation. So instead of mutating the data, we rebind the variable to new data.
flowchart
w["world"]
hello --> 4
hello = "hello"
hello_world = hello <> "world"
hello = "hi"
hello_world
hello
You'll notice that if you rebind hello
below to 4
, the Elixir cell above will still
have hello
bound to "world"
hello = 4
hello
Elixir and other programming languages allow you to represent information as data. What does that mean? Our job as programmers is often to translate project requirements into something that a computer understands. However, while computers can do computations in only a few seconds that would take our entire lives, they only do what we tell them to. So we have to provide incredibly explicit instruction.
The great thing about computers is they do what we tell them to. But, unfortunately, that means if something goes wrong, it's usually our fault.
Check out the Exact Instructions Challenge: Peanut Butter and Jelly for a messy example!
To provide instruction to the computer, you need to understand the language it speaks. Well, that's not entirely true. The actual language the computer speaks is in electrical signals represented as 0s and 1s. This is called binary. Instead of writing programs in binary We have programming languages like Elixir that allow use to write instructions in more or less plain English with some special words, characters, and restrictions.
Some programming languages look more like plain English, and other's definitely don't. There's even a programming language that let's you write a program as a cow!
MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO
MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO
MoO MoO Moo MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO Moo MoO MoO
MoO MoO MoO MoO MoO Moo Moo MoO MoO MoO Moo OOO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO
MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO Moo MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO
MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO
MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO
MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO Moo MOo
MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo MOo
MOo MOo MOo MOo MOo Moo MOo MOo MOo MOo MOo MOo MOo MOo Moo MoO MoO MoO Moo MOo MOo MOo MOo MOo MOo Moo MOo MOo MOo MOo MOo MOo MOo MOo Moo
OOO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO MoO Moo
Some programming languages operate closer to the machine code the computer uses, these are called low-level programming languages. They are great for optimizing performance but often much harder to read and understand.
Alternatively, other programming languages improve developer's quality of life by operating closer to human language. These languages are referred to as high level programming languages. Elixir is a high-level programming language. It's developer-friendly and powerful.
Whatever programming language you're using, you need to understand how to convert real-world concepts into instructions the computer understands. This lesson is going to focus on exactly that. So, how do we represent real-world information as something the computer understands? The answer is data. Depending on what type of information we want in the program, we use different data types.
For example, on your social media profile (Linkedin, Twitter, Instagram, etc), you often have to store your name, birthday, and some of your interests.
Your name might be in a data type called a string.
"Peter Parker"
The same goes with the month you were born.
"August"
Sometimes we can use different data types to represent the same information. For example, August is the 10th month, so we could use an integer instead.
10
You will learn all of the data types below and what kinds of information they are good at representing. Don't worry; this is just an overview of what you're about to learn. You are not expected to be familiar with any of these data types yet.
- Booleans
true
andfalse
values. - Integers whole positive and negative numbers such as
1
,345
,0
, and-98
. - Floats positive and negative decimal numbers such as
1.3
,31.54
,0.0
, and-65.2
. - Strings text information such as
"Hello, world!"
. - Atoms constants who's name is their value such as
:ok
and:error
. - Lists lists of information such as
[1, 2, 3]
. - Tuples store multiple items of a known size
{5, 2, 1}
. - Ranges ascending or descending series of numbers with a step such as
1..10
. - Keyword Lists lists of information with associated keys and values
[hello: "world"]
- Maps a key-value data structure
%{hello: "world"}
Now that you understand control flow and pattern matching, you're ready to tackle error handling.
Computers are nearly perfect. However, humans are not. Errors and bugs are part of life as a programmer.
Sometimes we can anticipate potential errors. For example, what happens when you try to create a Date with an invalid month?
Date.new(1998, 122, 21)
The module function returns an error tuple {:error, :invalid_date}
.
For the errors that we expect, Elixir developers established a pattern of using
:error
and :ok
tuples.
Date.new(1998, 12, 21)
Functions that anticipate errors return either
{:ok, data}
or {:error, reason}
flowchart
Function --> OK
Function --> Error
Error["{error: reason}"]
OK["{ok: data}"]
This allows us to call these functions, and then use pattern matching to handle the :ok
or :error
case.
flowchart
Caller --- Function
Caller --> A[ok instructions]
Caller --> B[error instructions]
Function --> Output
Output --- ERROR["{:error, reason}"]
Output --- OK["{:ok, data}"]
The Date
module expects that we'll sometimes call it with mismatched integers and it handles that
error specifically.
This enables predictable error handling. We can use case
to match on the :error
and :ok
tuples,
then handle them however is appropriate.
case Date.new(1998, 12, 21) do
{:ok, date} -> date
{:error, reason} -> reason
end
In the Elixir cell below, trigger the :error
case by changing Date.new(1998, 12, 21)
to an invalid
date.
case Date.new(1998, 12, 21) do
{:ok, value} -> value
{:error, reason} -> reason
end
Create a function convert_to_12_hour
which takes in an integer from 1 to 24. It will then return the equivalent time
on a 12 hour clock.
- 1 would become
{:ok, "1am"}
- 14 would become
{:ok, "2pm"}
- If the value given is less than 1 return
{:error, :time_too_low}
. - If the value given is greater than 24 return
{:error, :time_too_high}
While we can do our best to predict errors, it's truly impossible to predict them all. Some errors are not within our control, such as the malfunction of physical hardware, some errors result out of unexpected complexity and behavior.
Some errors result because of external systems, or malformed data, and some errors are because we spelled accelleration with one l instead of two. 🤦♂️
Whatever the reason, unpredictable errors happen. Infact, Elixir has a philosophy of "Let it crash" which you'll learn more about in future lessons.
There are two main categories of errors. The first is a RuntimeError. A runtime error occurs while the program is running. That's why it's called a runtime error.
The other is a CompileError . A compile error occurs during compile time. What is compile time? Compile time when the Elixir source code that you've written gets converted into binary machine code instructions for the computer.
Elixir is a compiled language, so it's instructions get compiled into binary machine code before being run. Alternatively, some languages are interpreted. Interpreted languages get converted into computer instruction as they run rather than upfront.
Whenever you encounter an error, Elixir attempts to provide information about what happened by raising an error.
What does raise an error mean? You may have noticed Elixir provides a variety of errors that help you understand what went wrong in your program. Whenever Elixir encounters an error, it stops the current execution and displays an error.
You can manually raise a RuntimeError error with the raise
keyword.
raise "Oh no!"
You can also trigger a CompileError by writing some invalid Elixir syntax. Because Elixir is compiled, it's able to pre-determine that the code is invalid before running it.
you(cannot(just(write(anything))))
You've already been introduced to several other kinds of errors. It's not important that you remember all of them, but you should try to understand what they mean when you see them.
- FunctionClauseError: a function is called with input that doesn't match any clause. i.e
Date.new("", "", "")
- ArithmeticError: when you misuse an arithmetic operator i.e.
1 + ""
- TokenMissingError: when you don't complete an expression i.e.
1 +
These are all runtime errors that occur under specific conditions. They help you determine the cause of your error better than a generic RuntimeError.
flowchart LR
RuntimeError --> FunctionClauseError
RuntimeError --> ArithmeticError
RuntimeError --> TokenMissingError
When executing code that could result in an error, you case use try
and rescue
to prevent it from crashing
the entire program's execution. The code in the try
block will run, and if it results in an error,
then the rescue
block will run. the rescue
block works like a case
statement where you can
pattern match on the error from the try
block.
try do
raise "oh no!"
rescue
_ -> "that's better"
end
In the above example we pattern match using the default case with underscore _
.
You could instead match on the specific error RuntimeError
try do
raise "oh no!"
rescue
RuntimeError -> "that's better"
end
Infact, you have the ability to decide which type of error to raise
.
try do
raise ArithmeticError, "Math can be hard sometimes"
rescue
ArithmeticError -> "Fortunately you have support!"
end
It's convention in Elixir to use bang !
in the name of a function that expects to raise an error.
For example, there's an alternative to Date.new/4
called Date.new!/4
. Instead of returning an
:ok
or :error
tuple it either returns a date, or raises an error.
Date.new!(1982, 06, 29)
Date.new!(11982, 06, 29)
In the Elixir cell below, raise
a TokenMissingError
In the Elixir cell below
- Create a
try
/rescue
block. - Cause a SyntaxError in the try block.
- Handle the SyntaxError in the rescue block and return
"phew!"
.
While theoretical performance is important, in practice it's best to test your assumptions.
There are a wide variety of tools that allow you to test performance.
You can use the built-in Erlang library :timer
's tc/1
function to measure the time it takes
to run a function in miliseconds.
{time, _result} = :timer.tc(fn -> 100 ** 1_000_000 end)
time
In practice, we'll usually lean on a library to test performance. The Benchee library allows us to check multiple function's performance and memory usage.
list = Enum.to_list(1..100)
tuple = List.to_tuple(list)
test =
Benchee.run(
%{
"list" => fn -> Enum.at(list, 50) end,
"tuple" => fn -> elem(tuple, 50) end
},
time: 1,
memory_time: 2
)
Kino.nothing()
Benchee is a popular library for measuring performance and memory consumption.
External libraries need to be installed to use them.
We use Mix to install Benchee.
Mix is a build tool that provides tasks for creating, compiling, and testing Elixir projects, managing its dependencies, and more.
flowchart LR
Benchee
D[Dependencies]
I[install/2]
Mix
Mix --> I
I --> D
D --> Benchee
Mix.install([{:benchee, "~> 1.0"}])
External libraries often have different versions, so we need to specify the version of the project we'd like to use.
flowchart
0[0.98]
1[0.99]
2[1.00]
3[1.01]
B[Benchee]
M[Mix]
M --> B
B --> 0
B --> 1
B --> 2
B --> 3
We can use the Benchee.run/2
function to measure the performance and memory consumption of some code.
Benchee.run(
%{
"example test" => fn -> 10000 ** 10000 end
},
time: 10,
memory_time: 2
)
Now, let's prove this to be true. We can use the :erts_debug.same/2
function from Erlang to
prove if two values are the same in memory. This is for demonstration purposes only, it's rare that
you would need to rely on this function.
Primitive data types like 1
are sometimes called immediate values. They are written
directly into the instruction rather through a reference or pointer. This means that 1
is the same
value as 1
.
:erts_debug.same(1, 1)
Non-immediate values are understood by the computer through a reference or pointer. This means
that non-immediate values such as [1]
are not identical.
:erts_debug.same([1], [1])
We can however, prove that a shallow copy of some data structure is the same underlaying data. Here we can prove that when two variables contain the matching non-immediate values they are not the same data structure.
a = [1]
b = [1]
:erts_debug.same(a, b)
And when we instead assign b
to reference a
they do share the same non-immediate data structure.
a = [1]
b = a
:erts_debug.same(a, b)
a
in a_tuple
and a
in new_tuple
share memory. This means they are structurally identical,
and we can prove it.
{a1, b1, c1} = a_tuple
{a2, b2, c2} = new_tuple
:erts_debug.same(a2, a1)
:erts_debug.same(a2, a1)
Elements prior to the modification in the list will be shallow copied.
Let's prove that
by updating a list and using :erts_debug.same/3
to show that elements prior to the modification
are shared.
a = {1, 2, 3}
b = {4, 5, 6}
c = {7, 8, 9}
b2 = {10, 11, 12}
# New_list shallow copies a from a_list and reuses c.
a_list = [a, b, c]
new_list = List.replace_at(a_list, 1, b2)
# The first element in new_list is a shallow copy of the first element in a_list.
# This means that the first element in both lists is structurally identical.
# true = :erts_debug.same(List.first(a_list), List.first(new_list))
# false = :erts_debug.same([a], [a])
# false = :erts_debug.same(Enum.take(a_list, 1), Enum.take(new_list, 2))
true = :erts_debug.same(Enum.drop(a_list, 2), Enum.drop(new_list, 2))
:erts_debug.same({1, 2, 3}, {1, 2, 3})
Enum.take([1, 2], 1)
We can also prove that the tail of the list is reused.
:erts_debug.same(List.last(a_list), List.last(new_list))
You may notice that sometimes your list of integers will print as a strange character. Notice
that the final element in the following is a '-\n'
rather than the expected [45, 10]
list =
for a <- 1..45, b <- 1..10, rem(a, 15) === 0, rem(b, 5) === 0 do
[a, b]
end
That's because internally Elixir represents lists of integers as Charlists.
However, if you use the list again, you'll see that '-\n'
still represents [45, 10]
internally.
Enum.map(list, fn [a, b] -> {a, b} end)