Joy of Elixir

Exercise Solutions

This section contains solutions for all the exercises in this book. Use this only if you're stuck!

Chapter 1

Exercise 1

Get Elixir to calculate the number of seconds in the day by multiplying the hours in a day by 60 twice. How many seconds are there in a day?

There are 24 hours in a day, so we can multiply by 60 twice like this to get our answer:

iex> 24 * 60 * 60
86400

Exercise 2

Calculate the average of these numbers: 4, 8, 15, 16, 23 and 42.

To calculate the average of these numbers, we need to add them all together and then divide that number by how many numbers there are, so 6.

iex> 4 + 8 + 15 + 16 + 23 + 42
108
iex> 108 / 6
18.0

Special thing to note here is that when we divide in Elixir, it will always give us a decimal representation of the number, rather than a whole number. This is why we see 18.0 instead of simply 18 here.

Chapter 2

Exercise 1

If we store the number of seconds in a day using this code: seconds = 86400, calculate using that variable how many hours there are in 30 days.

Tricky part here is that the variable is for seconds, but the answer we seek is for hours. To convert the number of seconds into hours, we need to a similar calculation to Exercise 1.1: take the number of seconds, divide by 60 to get minutes and then divide by 60 again to get how many hours are in a single day:

iex> seconds = 86400
86400
iex> seconds / 60 / 60
24.0

We know that there are 24 hours in a given day. So to get the number of hours in 30 days, we can multiply 24 by 30:

iex> 24.0 * 30
720

This gives us our answer: there are 720 hours in 30 days. Doing this whole calculation in a single line would look like:

iex> seconds / 60 / 60 * 30
720

It's possible to make this a little shorter too. At the end of this line, we're dividing by 60 and then multiplying by 30. Dividing anything by 60 and multiplying it by 30 is the same as dividing it by 2 (2 comes from dividing 60 by 30). The same could be said if we divided by 10 and multiplied by 5, or divided by 4 and multiplied by 2. So we can shorten this by dividing by 60 once, and then dividing by 2 to get the same answer:

iex> seconds / 60 / 2
720

Exercise 2

The line 5 / "four" shows an error. Think about why this error might happen.

The error that we'll see for this line is this:

iex> 5 / "four"
** (ArithmeticError) bad argument in arithmetic expression: 5 / "four"
:erlang./(5, "four")

Elixir is saying here that there's a "bad argument in arithmetic expression" -- which is a confounding way to say "what you're asking me to say doesn't make sense to do!". It is not possible to divide the number 5 by the word "four". These two things are incompatible, which is why we're seeing this issue.

Chapter 3

There are no exercises for this chapter.

Chapter 4

There are no exercises for this chapter.

Chapter 5

Exercise 1

Make a function which turns fahrenheit temperatures into celsius.

Early on in Chapter 5, we see a function which converts celsius into fahrenheit:

iex> c_to_f = fn (c) -> c * 1.8 + 32 end

This exercise is about going the other way: converting from fahrenheit to celsius. We need to reverse the order of operations here and then turn the operations into their opposites too. Where we add, we minus. Where we multiply, we divide:

iex> f_to_c = fn (f) -> (f - 32) / 1.8 end

We must include the brackets here on the first operation, otherwise Elixir will attempt to divide 32 by 1.8 first. Go on, try it without the brackets and see what happens.

We can run this function like this:

iex> f_to_c.(104)
40

If you ask Google "104 Fahrenheit in Celsius?", you'll see this is the right result:

Google result for
Google result for '104 Fahrenheit in Celsius'

Other numbers should work just as well too!

Exercise 2

Make a function which returns the number of seconds in the specified amount of days. For example, seconds.(2) should tell us how many seconds there are in 2 days.

We know that there are 24 hours in a day, 60 minutes in an hour, and 60 seconds in an hour. So to get from days to seconds, we need to multiply by 24, then 60, then 60 again. In a function, this would look like:

iex> seconds = fn (days) -> days * 24 * 60 * 60 end

We can then run this function like this:

iex> seconds.(1)
86400
iex> seconds.(2)
172800

Exercise 3

Make a function which takes two maps with "age" keys in them and returns the average age.

This function needs to take two maps, and the easiest way to do that would be to have them as two separate arguments.

iex> average_age = fn (person_1, person_2) -> ...
We can then take out the relevant age values from these maps with some pattern matching:

iex> average_age = fn (%{"age" => age_1}, %{"age" => age_2}) -> ...

This change of the function makes it so that the function only cares about the "age" keys in the maps, and will ignore the rest. From there, we need to calculate the average. The way we can do that is to add together all the numbers we have, and then divide the result by the count of the things we added together.

iex> average_age = fn (%{"age" => age_1}, %{"age" => age_2}) -> (age_1 + age_2) / 2 end

We can then run this function like this:

average_age.(%{"age" => 15}, %{"age" => 45})
30.0

It's important to note that both of the maps have to have the "age" key, otherwise this code will not work:

average_age.(%{"age" => 15}, %{"name" => "Izzy"})
** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/2

The following arguments were given to :erl_eval."-inside-an-interpreted-fun-"/2:

    # 1
    %{"age" => 15}

    # 2
    %{"name" => "Izzy"}

This error shows that the arguments that we passed do not match what the function expects. It helpfully shows us what we've passed, so that we can see for ourselves that the 2nd argument does not fit the criteria of needing an "age" key.

Chapter 6

Make a function that takes either a map containing a "name" and "age", or just a map containing "name". Change the output depending on if "age" is present. What happens if you switch the order of the function clauses? What can you learn from this?

For this, we can use a function that pattern matches on what keys are in the map. We'll call this function about because it's going to tell us about a person:

iex> about = fn
  %{"name" => name, "age" => age} ->
    "#{name} is #{age} years old"
  %{"name" => name} ->
    "I don't know how old #{name} is!"
end

We can call the function with a map that contains a "name" and "age" key:

iex> about.(%{"name" => "Ryan", "age" => 31})

This map matches the first clause:

iex> about = fn
  %{"name" => name, "age" => age} ->
    "#{name} is #{age} years old"
  %{"name" => name} ->
    "I don't know how old #{name} is!"
end

Or we can call it with a map with just a name key:

iex> about.(%{"name" => "Izzy"})

This map matches the second clause, because the first clause requires both a "name" and "age" key.

iex> about = fn
  %{"name" => name, "age" => age} ->
    "#{name} is #{age} years old"
  %{"name" => name} ->
    "I don't know how old #{name} is!"
end

The last part of the exercise asks: "What happens if you switch the order of the function clauses?". Let's try it. We'll reorder the function:

iex> about = fn
  %{"name" => name} ->
    "I don't know how old #{name} is!"
  %{"name" => name, "age" => age} ->
    "#{name} is #{age} years old"
end

And then we'll try to run it:

iex> about.(%{"name" => "Ryan", "age" => 31})

This map matches the first clause because the first clause only needs a "name" key to match its pattern:

iex> about = fn
  %{"name" => name} ->
    "I don't know how old #{name} is!"
  %{"name" => name, "age" => age} ->
    "#{name} is #{age} years old"
end

This may be unexpected; it may be expected to match the second clause, the one with both "name" and "age". But this will not happen, because the first clause is really relaxed: it only expects "name"

This is hopefully a good lesson in pattern matching in Elixir. A good rule to follow is that your clauses should be ordered from most-specific to least-specific. If you do not order your clauses like this, then you may have a clause matching before others unexpectedly.

Chapter 7

There are no exercises for this chapter.

Chapter 8

Exercise 1

Make a program that generates a very short story. Get it to take some input of a person, a place and an object -- using IO.gets/1 and combine all three into a little sentence, output with IO.puts/1

What we're looking to output to the screen is a string containing a person, a place and an object. Something like this:

I suggest it was [Colonel Mustard], in the [Ballroom], with the [Revolver]

First, we need to collect the pieces:

person = IO.gets("Whodunit? ") place = IO.gets("Where? ") object = IO.gets("With what? ")

We'll need to trim all of these, as when IO.gets/1 gives us our values it will include a new line at the end:

person = "Colonel Mustard\n"

This will lead to our output to look a little... strange:

I suggest it was [Colonel Mustard]
, in the [Ballroom]
, with the [Revolver]

We can strip it like this:

person = String.trim(IO.gets("Whodunit? "))
place = String.trim(IO.gets("Where? "))
object = String.trim(IO.gets?("With what? "))

(We'll see a cleaner way of writing this code in Chapter 10's exercise solutions!)

With everything collected, now we can output it:

IO.puts "I suggest it was [#{person}] in the [#{place}] with the [#{object}]"

The whole program looks like this:

person = String.trim(IO.gets("Whodunit? "))
place = String.trim(IO.gets("Where? "))
object = String.trim(IO.gets("With what? "))

IO.puts "I suggest it was [#{person}] in the [#{place}] with the [#{object}]"

If we put this code into a file called clue.ex and run it with elixir clue.ex, and answer all its prompts, we'll see the output we wished for:

I suggest it was [Colonel Mustard], in the [Ballroom], with the [Revolver]

Exercise 2

Ponder on what happens when you remove the IO.puts from the beginning of Line 3 in greet.exs and then run the program with elixir greet.exs. Think about how this would be different if you put that code into an iex prompt.

If we remove the IO.puts line from greet.exs, nothing will be output. This is different from the iex prompt, which will show us the string, with the name and age variables put in their right place:

iex> "Hello, #{String.trim(name)}! You're #{String.trim(age)}? That's so old!"
"Hello, Ryan! You're 31? That's so old!"

This is because iex is a Read-Eval-Print Loop, it will read in the code, evaluate it, and print (or output) whatever the code tells it to do. This is different from running the program greet.exs, because that only outputs when we tell it to do so. When we remove the IO.puts from the beginning of the line, we're telling it to evaluate the string, but not to output it.

Chapter 9

Exercise 1

Use a combination of Enum.map/2 and String.replace/3 to replace all the e's in these words with another letter of your choosing: ["a", "very", "fine", "collection", "of", "words", "enunciated"]

If we wanted to replace the letter "e" in the word "very", we can do that easily with String.replace/3. The three arguments are the string we want to change, what in that string we want to replace and what we want to replace it with.

iex> String.replace("very", "e", "a")
"vary"

To do this with a list of strings, we must employ Enum.map/2. The two arguments here are the word list, and then a function to run over each of them:

iex> words = ["a", "very", "fine", "collection", "of", "words", "enunciated"]
["a", "very", "fine", "collection", "of", "words", "enunciated"]
iex> Enum.map(words, fn(word) -> String.replace(word, "e", "a") end)
["a", "vary", "fina", "collaction", "of", "words", "anunciatad"]

The words have now all had their "e" letters replaced with "a", which makas tha word list look a littla stranga, don't you rackon?

We can write this a little shorter by using the capture operator:

iex> Enum.map(words, &(String.replace(&1, "e", "a")))
["a", "vary", "fina", "collaction", "of", "words", "anunciatad"]

Exercise 2

Use Enum.reduce/2 to multiply these numbers together: [2, 4, 991, 2543]

The final example of Enum.reduce/2 shows us how to add together a list of scores:

iex> scores = [74, 79, 89, 32, 79, 70, 32, 69, 76, 73, 88, 73, 82, 31]
[74, 79, 89, 32, 79, 70, 32, 69, 76, 73, 88, 73, 82, 31]
iex> Enum.reduce(scores, fn (score, sum) -> sum + score end)
947

We can adapt this to multiply together our new list:

iex> numbers = [2, 4, 991, 2543]
[2, 4, 991, 2543]
iex> Enum.reduce(numbers, fn (number, sum) -> sum * number end)
20160904

Chapter 10

Use your newfound knowledge of the pipe operator to re-write your solution to Chapter 8's first exercise.

As a little refresher, here's what our end solution to Chapter 8 looked like:

person = String.trim(IO.gets("Whodunit? "))
place = String.trim(IO.gets("Where? "))
object = String.trim(IO.gets("With what? "))

IO.puts "I suggest it was [#{person}] in the [#{place}] with the [#{object}]"

We have to read this code from the inside-out -- we read the argument that's passed to IO.gets/1, then we need to read IO.gets/1 itself to understand that, then we read the String.trim/1 function.

The pipe operator can make this code easier to understand:

person = "Whodunit? " |> IO.gets |> String.trim
place = "Where? " |> IO.gets |> String.trim
object = "With what?" |> IO.gets |> String.trim

Now our code reads left-to-right, just like a sentence.

For bonus points, we can take out this repetition and move it into a function:

question = fn (question) ->
  IO.gets("#{question} ") |> String.trim
end

person = question.("Whodunit?")
place = question.("Where?")
object = question.("With what?")

IO.puts "I suggest it was [#{person}] in the [#{place}] with the [#{object}]"

When we run this code again, it will work just the same. The function is the same, but the form is now a lot cleaner with the pipe.

Chapter 11

  • Can you make Elixir write a program for itself? Put the following code into a file called script.ex with File.write/2, writing to a file called generated.ex. Here's the code that should go in script.ex: IO.puts "This file was generated from Elixir". When we run elixir script.ex, we should then be able to run elixir that-file.ex and see our output.
  • Figure out what happens if you try to delete a file that doesn't exist with File.rm/1. Is this what you expected to happen?

Chapter 12

There are no exercises for this chapter.

Chapter 13

There are no exercises for this chapter.

Chapter 14

There are no exercises for this chapter.

Chapter 15

There are no exercises for this chapter.

Chapter 16

There are no exercises for this chapter.

Chapter 17

Add a new route that accepts a date in the format of 1987-12-04 as a parameter. Use your Person.age function to return how old this would make the person.

The first step here is to add a new route to People.Router, defined in lib/router.ex

defmodule People.Router do
  use Plug.Router

  plug(:match)
  plug(:dispatch)

  get("hello/:name", to: People.Hello)
  get("goodbye/:name", to: People.Goodbye)
  get("age/:birthday", to: People.Age)

  match _ do
    send_resp(conn, 404, "there's nothing here")
  end
end

This route goes to a plug that does not exist, and so we will need to define a module with an init and call function inside it:

defmodule People.Age do
  import Plug.Conn

  def init(options), do: options

  def call(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "You are #{age} years old!")
  end
end

Inside the call function here, we do not have an age variable yet. We'll need to calculate that using the Person.age/1 function... but that function takes a Person struct, and that struct must contain a value for birthday so that our calculation works. We need something in this shape:

%Person{birthday: "1987-12-04"}

But instead of a hard-coded birthday, we need the value from the parameter. Let's pull that out using pattern matching first:

def call(%Plug.Conn{params: %{"birthday" => birthday}} = conn, _opts) do
  conn
  |> put_resp_content_type("text/plain")
  |> send_resp(200, "You are #{age} years old!")
end

Now we will have a birthday value that we can use to construct a person struct:

def call(%Plug.Conn{params: %{"birthday" => birthday}} = conn, _opts) do
  age = %People.Person{birthday: birthday} |> People.Person.age

  conn
  |> put_resp_content_type("text/plain")
  |> send_resp(200, "You are #{age} years old!")
end

This will get us most of the way there, but if we attempt to run the code now we'll see it blow up:


[timestamp] [error] #PID<0.394.0> running People.Router (connection #PID<0.384.0>, stream id 3) terminated
  Server: localhost:4001 (http)
  Request: GET /age/1987-12-04
  ** (exit) an exception was raised:
      ** (FunctionClauseError) no function clause matching in Date.diff/2
          (elixir 1.10.4) lib/calendar/date.ex:577: Date.diff(~D[2021-02-03], "1987-12-04")
          (people 0.1.0) lib/person.ex:12: People.Person.age/1
          (people 0.1.0) lib/age.ex:7: People.Age.call/2
          (people 0.1.0) lib/plug/router.ex:284: People.Router.dispatch/2
          (people 0.1.0) lib/router.ex:1: People.Router.plug_builder_call/2

This exception is pointing squarely at our People.Person.age function, and at its use of Date.diff/2. This function takes two dates, but we've given it a date and a string. So we need to convert the birthday variable into a date!

def call(%Plug.Conn{params: %{"birthday" => birthday}} = conn, _opts) do
    age = %People.Person{birthday: Date.from_iso8601!(birthday)} |> People.Person.age

    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "You are #{age} years old!")
  end

This will then ensure that when the birthday goes through to People.Person.age that it's a date, and we can then compare two dates with Date.diff/2. A tricky little step, wasn't that?

It's also worth pointing out here that we use People.Person to refer to that module twice, but we could use only Person if we aliased the module at the top of Person.Age

defmodule People.Age do
  import Plug.Conn
  alias People.Person

  def init(options), do: options

  def call(%Plug.Conn{params: %{"birthday" => birthday}} = conn, _opts) do
    age = %Person{birthday: Date.from_iso8601!(birthday)} |> Person.age

    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "You are #{age} years old!")
  end
end

This makes our code a little bit longer vertically, for the sake of saving some horizontal length.