Elixir Beginner II — Tutorial
This tutorial was originally written for the Women Who Code Sydney workshop https://www.meetup.com/Women-Who-Code-Sydney/events/254987164/
Skill level for this tutorial:
This tutorial is aimed at people who have coded before. You should be familiar with concepts like variables, lists / arrays, simple data types, Classes/Modules and methods/functions.
If you are new to coding, you may want to try this tutorial: https://medium.com/@clairettran/elixir-beginners-tutorial-2f8548b2472d
What is Elixir?
Elixir is a functional programming language which started as an R&D project of Plataformatec and now used by companies such as The Bleacher Report, Pinterest, Moz, Expert360, Culture Amp and many others! (The syntax can appear Ruby-like at first and beginners may appreciate some of the similarities here)
What we’ll be covering
In this tutorial we will be covering a few topics:
- Data Types
- Complex Data Types — Map, List
- Modules and functions
- Pattern matching
- Conditionals
- Guards
- Pipe Operator
- Testing
- Creating a command line app
Installation
If you are working off your computer, for this tutorial, you’ll need to have
- Elixir installed (https://elixir-lang.org/install.html)
- Git
- Github account (to clone the repo)
- An IDE or Editor (e.g. Atom or Sublime)
For Windows users
This guide will help: https://www.youtube.com/watch?v=antnsMgA4Ro(see the 2:50 min mark and stop at 3:50 min mark)
Documentation
- You can refer to the docs at any time: http://elixir-lang.org/docs.html
- Elixir Cheatsheet: https://devhints.io/elixir
Step 1: Quick introduction
Step 1.1 The REPL
We will first open the terminal and try some things out:
Elixir has a REPL which you can try some code on too
In the terminal, type:
iex
You’ll get something like this
Now type:
IO.puts "hello"
Step 1.2 Data Types
The basics types we will run through are:
- Integers
- Floats
- Atoms
- Strings
- Booleans
- Tuple
We’ll cover Lists and Maps in the next step.
Feel free to try these in iex
if you want to, otherwise read through, as we will be coding in Step 2.
Integers:
Floats:
Operators:
Strings:
String Interpolation:
String concatenation:
Atoms:
An Atom
is similar to a Symbol
in Ruby and is a constant with it’s value being itself. Commonly used in Maps, Enums or return codes (e.g. in Elixir you may check for errors using {:ok, response}
Tuple)
Booleans:
Tuple:
Tuples are enclosed in { }
and can contain any number of elements, and of different types
They also are used in Elixir for response handling
Step 1.3 Maps and Lists
A List can contain different types and has other functions like flatten
and first
available. Lists are actually LinkedLists under the hood in Elixir.
For more info: https://michal.muskala.eu/2015/10/16/understanding-lists.html
Maps:
Functions available on Maps:
As well as Enum
functions, for example
The rest of the concepts will be covered in the code example we will be writing:
- Pattern Matching
- Conditionals
- Pipe Operator
- Testing
- Command line app
Step 2 Coding Example
Step 2.1 Clone the Repo
https://github.com/claritee/addressbook_cli.git
If you don’t have git
You can download the repo from here: https://github.com/claritee/addressbook_cli/archive/master.zip
If you get stuck
If you have trouble at any stage, the answers are available here: https://github.com/claritee/addressbook_cli/tree/answers
Directory Structure
You’ll the following directory structure
If you’re familiar with Ruby this has a similar structure. Below is a comparison between different project structures.
Directory structure breakdown:
mix.exs
contains the project definition and dependenciesmix.lock
this is the lockfile, which lists the dependencies and the versions used in the projectconfig
— within this directory,config.exs
contains config for the project (for example environment variables)lib
— this directory contains theaddress_book.ex
module which we will be use to write the command line app code in.test
— this directory containsaddress_book_test.exs
(the test file) andtest_helper.exs
(test helper functions)
For more details, see: https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html
Step 2.2 Modules and Functions
Let’s open up lib/address_book.ex
Here you will see the module
definition of AddressBook
. The syntax for a module
is something like the following
defmodule ModuleName do
# code
end
You’ll also notice a function
called main
defined in the module.
The syntax for a function
is
def function_name(args) do
# code
end
And for a private
function, this is defined using defp
In the main
function, you’ll also notice args \\ []
defined. This is a way to define default arguments in the function, in this case, if args
are not passed to the function, this will default to an empty list.
The arity
is basically the number of arguments the functions takes, so in this case main
has an arity
or 1
, also could be referred to something like main/1
Let’s now go back to the terminal and compile the app.
Step 2.3 Dependencies, Compile and Run
First download the dependencies, run at the root of the project:
mix deps.get
You should see the dependencies downloaded
Then compile the code
mix compile
You should see this
Next generate an executable to run
mix escript.build
You should see this
If you check your files, you should also see this file address_book
generated:
Let’s now run the app
./address_book xxx
You see xxx
as the result on the terminal.
Let’s now write some code.
You’ll notice a file called people.csv
in the project too:
We’ll be parsing this file in the project to find information.
Step 3. Parsing argument options
Step 3.1 Pattern Matching
This is a way to match an expression on the left hand side to the right hand side of the =
(which in Elixir is called the Match Operator
)
Here is an example with lists
In the example above, we determined the first item in the list head
and the rest of the list tail
using pattern matching
This is a technique that we will continue to explore in the tutorial
Let’s go back to lib/address_book.ex
and change the code to this
IO.inspect
is a way to inspect the contents of a variable or complex type
OptionParser
is a module that allows us to parse options passed on the commandline
You’ll also notice {opts, _word, _}
on the left hand side of the expression. We’re pattern matching
the result into params with the result of calling OptionParser.parse/2
(also note: _
is a way to denote an ignored variable)
Regenerate the executable program file and run the app:
mix escript.build
./address_book --file people.csv
You should see the following output
The result opt
is a Keyword List
of one item.
Try running again with another option, like the following
./address_book --file people.csv --find city --name Lily
You’ll see the result is
Which is also the same as
[{file: "people.csv"}, {find: "city"}, {name: "Lily"}]
The result has 3 elements, with keyword items (key-value pairs) in the list. You’ll also notice that the keys are Atoms
Step 3.2 Pipe Operator
Elixir has a pipe operator
which is used to pass the result of one function to another.
For example, let’s try this:
Here we first converted the String"hello"
to upper case, then called String.split/2
to convert this into a list.
Notice that the first argument into String.split
was ""
(which is actually the second argument of the function.
This is because the result of String.upcase/1
was passed as the real first argument of String.split
If we rewrote this, it’s the same as
Which, if we re-wrote this as functions is like this:
G(F(x))
Which is known as functional composition
in functional programming.
You can keep chaining functions using the |>
operator, let’s expand the example
Let’s now apply this to our code
Let’s add a private function to parse the args parse_args
(private functions are defined with defp
)
Which will be something like this
Next add another function to return a response (doesn’t do much just yet)
Now let’s refactor the code to first parse_args
and then return a response with response
The AddressBook
module should look something like this
Re-run
mix escript.build
./address_book --file people.csv --find city --name Lily
The output would look something like this
Step 4. Parsing the CSV File
Step 4.1 Decoding the CSV file
We now want to parse the CSV file people.csv
We’re going to be using CSV
from the core Elixir library https://hexdocs.pm/csv/CSV.html
Let’s update the code to read the csv file and pass this to CSV.decode
Update the response
function
Then regenerate the executable and run
mix escript.build
./address_book --file people.csv
You should have this
According to the docs for decode
, the function should: “Decode a stream of comma-separated lines into a stream of tuples.”
So now we need to loop through each Tuple
Change the response
function to:
Re-run on the command line
mix escript.build
./address_book --file people.csv
You should get this as a response
The result is a Tuple
consisting of an Atom
( :ok
) and a List
representing each line
Step 4.2: Your turn
Change the response
function and use the elem
function to loop through the result to only show the List in the results
See: https://hexdocs.pm/elixir/master/Kernel.html#elem/2
The result should be
Step 4.3: Refactor to a function
Let’s refactor this, so that we loop through an return a Map
to represent a person with keys name
, age
and city
Firstly create a function
Now call this function
Then re-generate and run
mix escript.build
./address_book --file people.csv
The result should be
Let’s use Enum.map
to return a list in the response
function.
(The function defined with fn
in Enum.map
is an anonymous function).
Now change the main
function
These anonymous functions can be refactored further. For example
Can be rewritten as
The item being looped over is replaced by &1
The function being called has &
in front of it, removing the need for fn x -> end
Step 4.4 Your Turn:
Now refactor the response
function and how to_person
is invoked within Enum.map
(the last line within the function)
Step 5. Testing
Step 5.1 Run Tests
In the terminal, run the following
mix test
You see some test failures (see the bottom of the output)
Let’s take a look at the test file test/address_book_test.exs
What do you see?
- The test uses
ExUnit.Case
(https://hexdocs.pm/ex_unit/ExUnit.Case.html) doctest
specifies which module is being tested- There are 4 tests defined, enclosed in blocks like this
test "..." do
# code
end
- Each assertion (in the
assert
statement) tests the result matches an expected value
Step 5.2 Making Tests Pass and Conditionals
Let’s try to make the first test pass
If you look at lib/address_book.ex
:
Right now, the code is only check the --file
option that is being passed, but not the --find
option
Let’s update this to check for the --find
option
Remember that the options from OptionParser.parse
return a Keyword List
(from the previous example above)
To access the find
option value, we would do this via opts[:find]
So if we change the code to:
if
statements can also be rewritten when the result can fit on one line too. For example the above can be rewritten
Now run the tests
mix test
You will the following
To run one test
mix test test/address_book_test.exs:6
You should get the following
Step 5.3 Using Case
Let’s now try to make the next test pass.
The next test we need to make pass is this one — where we are trying to find the oldest person.
Let’s go back to the lib/address_book.ex
Since we now have more than one value for the find
option, let’s change the if
condition to test for more than one value
We’re trying to match when the find
option is total
or oldest
If the value does not match, then the catch all _
will catch anything that falls through.
Create a function to find the oldest
Now, change the case
statement to use the find_oldest
function
After that, run the next test:
mix test test/address_book_test.exs:9
You should get this result
Let’s test this on the command line
mix escript.build
./address_book --file people.csv --find total #result: 5
./address_book --file people.csv --find oldest #result: Tom
The next test is to find the city that Lily lives in
Define a function to find the city a person lives in in lib/address_book.ex
Hint: Use Enum.find
https://hexdocs.pm/elixir/master/Enum.html#find/3 and Map.get(:city)
Now update the case
statement
Then run the next test to see if this works
mix test test/address_book_test.exs:13
The result should be
Then test on the command line
mix escript.build
./address_book --file people.csv --find city --name Lily # Berlin
Lastly, we want to ensure the last test works, this test attempts to find the person who lives in Melbourne
Create a function find_person
in lib/address_book.ex
Then update the case
statement
(Hint: use Enum.find
and Map.get(:name)
)
Then test:
mix test
Result:
Then run on the command line
mix escript.build
./address_book --file people.csv --find name --city Melbourne # Amy
Your code should look something like this
Step 6. Guards
In Elixir, functions can also have guards to match on cases
Let’s demonstrate with an example
Step 6.1: Refactor
Let’s move the case
statement to a separate function
Now refactor the case
statement to use this
Run tests to see that things still work
mix test
Let’s break this down further, to pass all options (this will set up a use for guards)
Change the response_do
function to
Then change the response
function to
Run mix test
to check that things still work
Step 6.2: Define a Guard
Let’s first tackle the case where the find
option value is total
Let’s change the response_do
function to this
Now create another function also called response_do
above the previous one
Then update the case statement to remove the total
condition.
The code should look like
Now run tests to see if this worked. Run:mix test
So when the find
argument is total
, the code will match on the function that checks this, i.e. when the guard
is when find == "total
Step 6.3 Your Turn
Now do the same for when the value of find
is oldest
Step 6.4 Convert the rest
Let’s define a catch all guard
Now convert the others, so now you should have something like this
Run mix test
to check that the code still works.
We can keep refactoring here. Notice that we’re calling another method find_oldest
, find_name
and find_city
.
These cases can be updated to contain the code in those functions.
For example, moving the code from find_oldest
to the response_do
function that has the guard on oldest
becomes
Run mix test
to check the code still works.
Now refactor the rest, you should have:
Your module
should look like this now
Lastly, run mix test
to test!
Step 7. Pattern Matching Functions
Another approach on the above is pattern matching
the arguments for functions instead.
Step 7.1 Refactor
Let’s try this with an example
Change the function response_do
where the guard is when find == "total"
Now change the response
function to use this
Now run the first test to check that the code works
mix test test/address_book_test.exs:6
What we did was match on the argument which is opts
: Keyword list
with keys file
and find
Next let’s do the same for the response_do
function that checks for oldest
Run tests for this to check things are working
mix test test/address_book_test.exs:9
Let’s convert the guard where we’re looking for the city
that a person of name
lives in, where the guard is when find == "city"
Run tests:
mix test test/address_book_test.exs:13
Step 7.2 Your Turn
Now convert the last guard, where we’re looking for the name
of the person for a city
they live in, where the guard is when find == "name"
Then run all the tests to check
mix test
Now run the app
mix escript.build
./address_book --file people.csv --find oldest
./address_book --file people.csv --find city --name Lily
./address_book --file people.csv --find name --city Melbourne
./address_book --file people.csv --find total
You’ve reached the end of the tutorial. Hope you enjoyed it!