Elixir Beginner II — Tutorial

Claire Tran
15 min readOct 7, 2018

--

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

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

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.

Source: https://pragdave.me/blog/2018/06/02/project-structure.html

Directory structure breakdown:

  • mix.exs contains the project definition and dependencies
  • mix.lock this is the lockfile, which lists the dependencies and the versions used in the project
  • config — within this directory, config.exs contains config for the project (for example environment variables)
  • lib — this directory contains the address_book.ex module which we will be use to write the command line app code in.
  • test — this directory contains address_book_test.exs (the test file) and test_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 responsefunction 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?

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!

--

--

Claire Tran
Claire Tran

Engineering Leader | Writer | Speaker | Traveller. Passionate about growing opportunities for people in Tech.

No responses yet