Part One: Introductions

  1. But who is this book for, really?
  2. Elixir? Isn't that something you drink?

Part Two: And away we go

  1. Appeasing the masses with code
  2. Now, where did I put that value?
  3. Lovely lists
  4. Marvellous maps
  5. Funky functions
  6. Pattern matching
  7. Part 2: Recap

Part Three: Building on the foundations

  1. Working with Strings, input and output
  2. Working with lists
  3. Working with maps
  4. Working with files
  5. Conditional code
  6. Finding more files
  7. Modules and Structs
  8. Part 3: Recap

Part Four: Real world Elixir

  1. Introduction to Mix
  2. Mix dependencies
  3. Automated testing
  4. Testing with ExUnit

The Appendixes

  1. Setup and Install
  2. Exercise Solutions

About "Joy of Elixir"

Joy of Elixir came about because I saw that there was not very much when it comes to absolute beginner material for learning Elixir. There's the excellent Getting Started guide on elixir-lang.org and the Programming Elixir book, but those feel like they're more targeted towards experienced programmers. They teach Elixir with a lot of assumed knowledge about programming languages. They're great books, but they're only great books for experienced programmers.

There's also the wonderful Elixir School site that serves well as a reference guide to the features of Elixir, but what newbies really need is a gentler introduction to Elixir.

Joy of Elixir avoids assuming you know anything about programming while teaching you about your first programming language: Elixir.

It seemed like there is a vast, empty, cavernous void where there should be something like the excellent Learn to Program book by Chris Pine. That book is for another programming language called Ruby; but there feels like there should be an equivalent to that for Elixir.

We have people completely new to programming wanting to learn Elixir — because people who have learned Elixir already told them about it and how cool it is! — but the support is not-quite-there yet. So this is an attempt to fill that void. Essentially a response to: "Why won't somebody think of the newbies?". Well, someone is thinking of the newbies.

I want Joy of Elixir to be the go-to-resource for teaching people (yes, that means you!) programming for the very first time using Elixir. I want you to experience the joy that Elixir (and programming in general) can bring to people. I want you to feel like you have power over the machine because of the knowledge contained within this book.

I want you to feel competent as our future's potential computer programmers. I want you to feel like you can become a programmer. This isn't stuff a "chosen few" can do. You're capable of learning this too.

Who did this?

The first Joy of Elixir thing you probably saw was the logo. It's only fair that this gets mentioned first, since it's the first thing. This logo was designed by Brandon Weaver. Thanks Brandon!

The Joy of Elixir project was started by Ryan Bigg (that's me). I love teaching people about programming and I wanted to write something to teach programming-first-timers about programming. Elixir is a great first language to use as it is simple to learn — especially if you have a good book leading the way!

I came up with an idea for this book while working at Culture Amp. I was mentoring a junior developer — Ramya Ravindranath — and she wanted to learn about Elixir, but found Programming Elixir a little difficult to get through. Around about the same time, one of my brothers-in-law — Dean Lynch — wanted to learn programming and I thought that he should learn Elixir... but then realised there wasn't a beginner's friendly book for people like him!

So really this book is written for people like Ramya and Dean. I have them in mind a lot when I'm writing this book.

License

This book is licensed under the Creative Commons Attribution Share Alike 4.0 License. This means it is free to share. You can grab the source of this book from this book's GitHub repository.

So please do share it freely amongst your friends, associates, workers, pets, lovers, etc.

Part One: Introductions

I. But who is this book for, really?

On the opening page you might've seen this:

Joy of Elixir is a gentle introduction to programming, aimed at people who already know some things about computers, but who have little-to-no programming experience. If you think you don't know enough about computers, well you got here already and that's enough!

Or you might've just gone straight to the "read it online" link and missed that. Anyway, now that I have your attention and you've read that little paragraph you might be thinking: but who is this book for, really? Well, let me lay it out plain and simple like:

This book is for you.

This book is for anyone who has ever wanted to learn to program. You might've attempted it before. You might not have. You might've read some very dry books on programming and been turned off programming altogether, but now you're back here. I'm glad you're still with us. Thanks for giving us all another shot.

If the extent of your programming experience is using functions like SUM(A1:A23) in Microsoft Excel to sum up all the numbers in a few rows, then that's fine. If you have no idea what I'm talking about, then that's fine too.

The extent of your programming knowledge might just be telling the computer to always put a signature at the end of your emails. Something official looking with your name, job title and phone number. There might even be a pithy quote at the end. Yes, even that is programming. You've given the computer an instruction and it's dutifully carrying it out until told otherwise.

You're reading this book on a computer device of some description and that's a perfect start. You probably know what a web browser is. You have some idea that programming lets you boss the computer around and cut down on the repetitiveness of some computer-based tasks. That's brilliant. You'll do well here.

Now with that out of the way, let's look at what this Elixir thing is.

II. Elixir? Isn't that something you drink?

Elixir the programming language is very different from an elixir which Wikipedia defines as:

a clear, sweet-flavored liquid used for medicinal purposes

And for the programming language variant it says:

Elixir is a functional, concurrent, general-purpose programming language that runs on the Erlang virtual machine (BEAM).

The first explanation seems like it is written in easy-to-understand perfectly-cromulent English. You read each word and you understand it as much as each other word. The second explanation is a stream of words that your nerdy friend would espouse right after pushing up their glasses and they'd probably prefix the sentence with "Well, actually [pause for dramatic effect]".

Sure, some of the words you'd understand, but the rest is nerdy-gobbledegook.

What the devil does "functional" even mean? Does it mean that it works? Well I sure would hope it works! Otherwise, what's the point of this book? May as well close it now. There can't be any use of using a programming language that isn't functional!

Concurrent? What? Things happen at the same time? Why is that important?

What even is an Erlang? Is that like Klingon?

Furthermore, what is a virtual machine and what makes it different from a regular machine? Does it have anything to do with virtual reality?

What is a BEAM? I've seen the Olympics, is it like the balance beam? Or is it like a light beam? Or does it somehow aid in the architecture of buildings? You'll discover later on what it means, but right now it is not as important as the description makes it seem.

It is a sentence very clearly written by one of your nerdy friend's compatriots for all the other nerdy people to understand. They'd all read the sentence and nod sagely and say stuff like "ahhh yes that is correct and succinct and easy to understand". That is because they are nerds and they live this stuff. You are new and we should at least make some effort not to scare you off with such talk!

A Normal-human-friendly explanation is required

Never mind the nerd-compatible explanations of what Elixir-the-programming-language is. Those are written for the nerds. Let's try this one on for size:

Elixir is a programming language that humans use to ask the computer to perform certain actions.

That's a pretty good explanation. I can say that because I am writing the book. I get to write whatever I want: that's how this works. I write, you read.

This new explanation uses simple English words to describe what you would use Elixir to do. It doesn't explain its fancy features using words like "functional" and "concurrent", and it certainly does not mention anything about BEAMs or machines, virtual or not.

The explanation is so good that it could be used to explain any programming language in existence. So it is probably too generic. It's like the black-and-white labelled food you find on the bottom shelf at the supermarket for a dollar. It does the job, but it doesn't quite sit well in your stomach soon after.

Ok, so let's re-vamp it! Simple Explanation Of Elixir The Programming Language, Version Two, here we go:

Elixir is an extremely fun and easy-to-use programming language that humans use to ask the computer to do things. You can write programs that are easy-to-understand for humans and computers alike. Writing Elixir is a joy.

Oh yeah! That's so much better. It gets the people going. Speaking of which, a small crowd of a few hundred people has gathered around. "Joy? I like joy!", they shout. Then they say: "We want to see some Elixir code! Enough talk!". Uh oh, the masses are getting rowdy. I better give the people what they want.

Part Two: And away we go

1. Appeasing the masses with code

The masses have sat through the best part of a chapter (at least) absorbing everything that we've talked about. But they're a little rowdy because we've been talking about programming languages without actually doing any programming. Well, that's completely understandable! I would be just as annoyed if I wasn't the one writing the book. I know what's coming but they don't.

OK, let's try appeasing the masses. Here's some code:

"Hello, World!"

The masses are silent. Their stares harden. Their spokesman — who they've nominated while I thought of this snippet — says (while systematically tearing down the fourth wall): "That's just a bunch of quoted words! Just like this one that I'm speaking."

The crowd agrees.

Ok, you're right. It is some quoted words, but they're quoted words that both humans and computers can understand. That's pretty cool. Well, I thought so, at least.

The crowd of humans arrayed before us know the words and that the words have meaning behind them and form an understandable sentence. The computer knows only the individual letters or symbols in the words, and cares not that the words have a meaning or that the words form an understandable sentence. "Those things are only important to the humans", it thinks, without acknowledging the Machine Learning zeitgeist that has sprung up recently.

When we say the sentence to the masses, they can easily repeat it back to us. To speak to the computer, we need to open up a prompt for the particular language we want to use to converse. Different languages have different prompts. We can't just speak to the computer to program it. Not yet, anyway.

In this case, we want to talk Elixir and so we can open a prompt with the iex command. iex stands for "Interactive EliXir". To open a prompt, we'll need to first open our terminal or command prompt application. Once that's open, then we can type iex into that window and press Enter to start our iex prompt.

When it starts, the prompt says:

Erlang/OTP 23 [erts-11.1.1] [source] ...

Interactive Elixir (v1.10.4) - ⏎
  press Ctrl+C to exit (type h() ENTER for help)

We can safely ignore the output from the prompt so far. It is just giving us some nerdy information. The last line shown here tells us that we're running Interactive Elixir "v1.10.4". This tells us that we're running the version of Elixir. It tells us that we can hit Ctrl+C to exit -- this will stop the iex prompt and return us back to our terminal's prompt).

The next thing the computer says is:

iex(1)>

This tells us that the computer is now listening for our instructions, eagerly awaiting them. It's prompting us for input. Let's give the computer our sentence again, pressing enter at the end of the line:

iex(1)> "Hello, World!"
"Hello, World!"

On the first line here, we're giving the computer an instruction that looks like a regular sentence. This is because it is a regular sentence. In computer-nerd-terminology, we refer to this double-quoted collection of symbols as a string. It's easier to think of it like a string if you think of all of the sentence's parts being connected by an actual string:

String connected with string
Figure 1.1: String, connected with strings!

The computer then takes in this string, interprets it and tells us how it interpreted the string. In this case, the computer is just parroting our sentence/string back to us. The computer can do a lot more than this, believe me. Computers wouldn't be very good if all they did was parrot back to us.

The masses are now getting fidgety. So far we've shown just the one line of code. Twice, yes, but it's still just one line of code. And the computer is prompting us with this line.

iex(2)>

The computer hungers for more input. The masses hunger, too. Fortunately for me (and you), Elixir can do much more than parrot.

Mathematical!

Ok, so the computer can parrot things. But what else can it do? How about we ask the computer to do some simple mathematics?

iex> 2 + 4
6
iex> 3 - 6
-3
iex> 4 * 12345
49380
iex> 1234 / 4 + 2 - 12 * 3
274.5

This appeases the masses, slightly. They're a fickle bunch. The computer is now no longer parroting things back to us. It's instead calculating the not-so-advanced mathematical equations we're giving it, and then giving us the right numbers. If you're not convinced these are the right numbers, I would encourage you to get a pen and paper and to puzzle them out yourselves. You'll find out that the computer is right. Good computer!

The masses realise that Elixir is now built for more things than simple parroting. Elixir can do calculations too! We've used the symbols +, -, *, and / here, asking the computer to add, subtract, multiply and divide, in that order. You might think to use x to multiply, but that has a different meaning as we'll see later on. You should use * when you wish to multiply things, as that is what Elixir expects you to do.

What we aim to achieve in this book

Okay, now that the masses are appeased, let's take a moment to talk about what this book is going to cover and what you're going to get out of it.

The first 12 chapters are going to cover some Elixir basics. Yes, that seems like a lot of chapters but most of these chapters are fairly short; just a couple of pages long. It's about 50 pages in total. These chapters will cover the things that you should know before you can write any Elixir program of a decent length, and it gives you a good tour of the language. These chapters are important because they will build a solid foundation for your Elixir experience.

Chapter 13 onwards will focus on building larger programs using the techniques that we've learned in the first 12 chapters. We'll combine things from earlier chapters and use them altogether and we'll even get introduced to even more things that the Elixir language does.

At the end of each chapter, there will be a set of (optional) exercises that you can do, using things that you have learned in the chapter. I strongly encourage that you try these out at least. If you're unable to do them, don't sweat it. Just try again later on.

With all that said, let's continue looking at what Elixir can do for us, starting next with making Elixir remember things for us.

Exercises

  • 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?
  • Calculate the average of these numbers: 4, 8, 15, 16, 23 and 42.

Note: You can find solutions to all of this book's exercises in the solutions section at the back of this book.

2. Now, where did I put that value?

We've now seen that Elixir can handle words and numbers easily. But what else can it do? Well, it can remember things.

iex> sentence = "A really long and complex ⏎
sentence we'd rather not repeat."

"A really long and complex ⏎
sentence we'd rather not repeat."

iex> score = 2 / 5 * 100
40

iex> x = "not for multiplication"
"not for multiplication"

As long as we leave iex running, Elixir will remember that sentence is "A really long and complex sentence we'd rather not repeat." and score is 40, and x is "not for multiplication". At any point in time, we can ask it what sentence or score or x is and it will tell us:

iex> sentence
"A really long and complex sentence we'd rather not repeat."
iex> score
40
iex> x
"not for multiplication"

sentence, score and x here are variables, and they're given that particular name because the thing that the computer remembers with that name of sentence or score or x can vary. Remember when we talked about multiplying numbers and we saw that we couldn't use x to multiply? The reason for this is because Elixir would read x as a variable, not as a suggestion to multiply the numbers on either side. So we will continue using * instead.

We can tell the computer to replace its definition of a variable with another one. This is often referred to as re-assignment. Let's replace (or re-assign) the sentence variable's definition:

iex> sentence = "An even longer and significantly more complex sentence ⏎
that we might be ok with repeating, if the mood takes us."
"An even longer and significantly more complex sentence ⏎
that we might be ok with repeating, if the mood takes us."

Then when we ask the computer what sentence is, it will have forgotten the old sentence and will only know the new one:

iex> sentence
"An even longer and significantly more complex sentence ⏎
that we might be ok with repeating, if the mood takes us."

Now the masses are looking chirpier. Some are even smiling! Isn't that wonderful? Let's create another variable called place:

iex> place = "World"
"World"

The computer will now remember that place is "World". "But what's the use of just setting a variable like this? Why make the computer remember at all?", Isodora (the spokesperson) says. "Spokesperson, thanks", says Isodora, breaking the 4th wall again. Okay, Isodora (the spokesperson) said those things. By the way, we have since learned the spokeswomanperson's name is Isodora, and so we've done some variable creation of our own in our own brain: spokesperson = "Isodora". Woah. "Just call me Izzy", says IsodoraIzzy. Ok, we will -- making our brains reassign the spokesperson variable: spokesperson = "Izzy".

Ok, Izzy's right. We should do something meaningful with this variable. Ok, Izzy, how about this? [cracks knuckles]

iex> "Hello, #{place}!"
"Hello, World!"

The masses go nuts. It's pandemonium! Then after a few shushing motions with my hands (which are completely ineffectual) and a quick "OI!" from Izzy, they're (mostly) quiet again.

"What just happened?", asks Izzy? But you, Dear Reader, had asked that question already. Izzy didn't hear because of the crowd and the sheer, unbounded pandemonium.

What on earth are those funky characters around place? You can think of them as a placeholder. Nerdier-types would call it interpolation. It tells the computer that we want to put the place variable riiiiight here, right after Hello and the space, and right before !.

Interpolating a variable into a string
Figure 2.1: Interpolating a variable into a string

The computer takes the sentence that we give it, realizes that there's that funky #{} placeholder indicator, and it remembers what place was and puts it right there. Pretty nifty, eh?

Exercises

  • 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.
  • Create a variable called name, store a string in it and place the value of that variable in another string.
  • The line 5 / "four" shows an error. Think about why this error might happen.

3. Lovely lists

But what else can Elixir do? We've seen that it can handle strings and it can handle mathematical equations with ease and it can remember things, but what else? Well, what if we had a list of things that we wanted the computer to remember? And we wanted the computer to remember this list of things in a particular order?

We could do this by assigning each of the things to a unique variable:

iex> first = "fish"
"fish"
iex> second = "ham"
"ham"
iex> third = "eggs"
"eggs"

But then we would need to remember that the things we want are stored in the variables first, second and third. And what if we — as humans — forget what position we were up to?

iex> fifth = "bread"
"bread"

Disastrous! This simply won't do.

A better solution to getting the computer to remember a list in Elixir is to actually tell it what we're storing is a list. To do this, we wrap our list in square brackets ([]) and separate each item in the list with a comma:

iex> shopping_list = ["fish", "ham", "eggs", "bread"]
["fish", "ham", "eggs", "bread"]

Just like we need to start and end a string with double quotes, we need to start and end a list with square brackets ([]).

The computer will now remember our entire shopping list in one variable (shopping_list), rather than remembering it across multiple variables. Now we have a one-stop-shop that we can go to for our shopping list.

We can use more than just strings when we're creating lists too. Let's say we want to have a list of the last temperatures (in celsius) of the last five days. We could represent this data like this:

iex> temperatures = [15, 17, 19, 20, 20]

In fact, anything in Elixir can be an item in a list.

Izzy, impressed with Elixir's ability to keep a list of anything her heart desires, has started taking the names down of the people who are in the crowd. Her computer materialised from nowhere, or perhaps from somewhere in the throng nearby. It was too quick to really notice. She starts taking down names, fingers flying across the keyboard:

iex> those_who_are_assembled = [
"Izzy",
"The Author",
"The Reader",
"Juliet",
"Mary",
"Bobalina",
"Charlie",
"Charlie (no relation)"
]
["Izzy", ...]

And so Izzy continues. She disappears into the crowd directly to our right, and somehow re-appears within mere seconds on our left. This is despite the density of the crowd. Amazing stuff. She then asks: "Hey Mr. Author Guy, I want to also track people's ages so that I can do some statistics on who exactly is assembled here. Oh, and I want to track their gender too. But a list doesn't seem very suitable for this. How can I do this?"

Well Izzy, there's more than one way you could do this. If all you wanted to track about a person was their name, their age and their gender, you could make each item in the list its own list:

iex> those_who_are_assembled = [
...> ["Izzy", "30ish", "Female"],
...> ["The Author", "30ish", "Male"],
...> ]

These lists-inside-of-lists would be called sub-lists. You would then have to keep in mind that the first item in each sub-list is the name, the second is the age and the third is the gender. What if you (or someone else, using your computer) forgot the order and started adding in people, with age and gender swapping positions?

iex> those_who_are_assembled = [
...> ["Izzy", "30ish", "Female"],
  ... a long time passes ...
...> ["Izzy the Younger", "Female", "20ish"],
...> ]

Pandemonium again! We need a better way of enforcing what data we're collecting for each person. We need a better data structure than lists within lists. Read on to find out what that data structure is!

4. Marvellous maps

Back in the last chapter, Izzy tried tracking information about the people assembled using some lists within another list:

iex> those_who_are_assembled = [
...> ["Izzy", "30ish", "Female"],
  ... a long time passes ...
...> ["Izzy the Younger", "Female", "20ish"],
...> ]

But as we can see here, the order of the items within the list can be input incorrectly due to human error. We need something that helps prevent human error like this, and if that something could helpfully indicate what each item in the list was — by giving it a kind of a name — then that would be good too.

For that, we can use a map. Just like a regular map, maps in Elixir can tell us where to find things, if only we knew where to look.

Let's look at how we could collect a single person's data using a map, using another random member of the crowd as an example:

iex> person = %{"name" => "Roberto", "age" => 56, "gender" => "Male"}
%{"age" => 56, "gender" => "Male", "name" => "Roberto"}

Maps start with a % and enclose their data in a warm curly-brace cuddle. In a map, we structure data using keys and values. The thing to the left of the => is called a key and the thing to the right is called a value.

a key and its value
Figure 4.1: A key and its value

Just like on a map of something like a city or a walking trail, a key here tells us where to find a particular value. If, for instance, we wanted to know the age of this person, we can use the key "age" to pluck out that value. We'll see that in a moment.

Unlike strings and lists where we can use a single character at the start and end to show Elixir where the starts and ends are -- double quotes for strings, square brackets for lists -- we need to use that "curly-brace cuddle" for maps. We need to use %{ to tell Elixir where a map starts and } to show where it ends.

You might notice here that the computer has taken in our map and returned the keys in a different order to the one that we specified. The order of keys doesn't matter in a map at all, unlike in lists where order does matter. It would be pretty strange to spend all the time ordering the list only for Elixir to return it in a non-sensical order. For instance, if we had a list like this:

favourite_people = ["The Reader", "Izzy", ...]

This list indicates that "The Reader" is the 1st favourite person, and that Izzy is a (close) second. The order here matters. For maps, it doesn't matter what order the keys and their corresponding values are in because the map would still be the same:

iex> person = %{"gender" => "Male", "age" => 56, "name" => "Roberto"}
%{"age" => 56, "gender" => "Male", "name" => "Roberto"}

The position of the "name" key within a map has no meaning. The keys for a map can be in any order.

You may now be thinking about how to get the data back out of a map once you've put it in. We talked about how to "pluck" that value out before, but didn't see an example yet. In order to access a value from a map we need to know the corresponding key. Once we know the corresponding key, then we can use [] to pluck that value out of the map. Think of it like the claw from a skilltester, diving in to pick out the value.

skilltester
Figure 4.2: Skilltester

Unlike a regular skilltester, these square brackets aren't rigged to drop the value mere inches from the chute; these square brackets have an iron grip. For instance, if we wanted to get out the value of "name" for person we can do:

iex> person["name"]
"Roberto"

And if we want to get the age, we would use the "age" key:

iex> person["age"]
56

Izzy lets out a thoughtful "Mmmmmmmmmm" in something very close to an agreement. She likes maps. Now Izzy can know the exact data that we're collecting about the assembled masses, without a concern for how the data ordered, and then use that for later on. We don't know yet what Izzy has in mind for the data, but she's collecting it for a reason. Or at least, it seems that way.

To collect data about all the people assembled here before us, we can create a list of maps:

iex> those_who_are_assembled = [
...> %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"},
...> %{"name" => "The Author", "age" => "30ish", "gender" => "Male"},
...> %{"name" => "The Reader", "age" => "Unknowable", "gender" => "Unknowable"},
...> ]

As we can see from this example, lists can contain more than just numbers and strings: we can use maps too!

This list of maps containing the crowd's information is immune to the problem we were seeing at the start of this chapter. It doesn't matter what order the key-value combinations of age, name or gender are placed -- the maps will contain the right information at the right spots. As an additional bonus, we can refer to these values with their keys, rather than having to remember their positions.

Other types of keys

Before we go onwards, I'd like us to dwell a bit longer on maps. So far, we've seen that maps are comprised of a series of keys and values. Those keys in our maps have only been strings, but the values have been a mixture of strings (for name and gender) and numbers (for age). This kind of hints at what maps are capable of storing, but I would like to cover a few more cases so we can really get the hang of these map things.

Truth be told, in Elixir the keys and values can be whatever you wish them to be. Keys can be numbers, strings, lists, and more. Values can be anything as well. Let's say we asked ten people to pick a number within the range of 1 to 5. You can use number keys to represent the numbers that people picked:

iex> choices = %{
  1 => 4,
  2 => 1,
  3 => 2,
  4 => 2,
  5 => 1,
}
%{1 => 4, 2 => 1, 3 => 2, 4 => 2, 5 => 1}

Then you can very easily find out how many people chose the numbers 1 or 2 by using those skilltester-claw-like square-brackets:

iex> choices[1]
4
iex> choices[2]
1

So as we can see here, we can easily use numbers as both the keys and values within a map and Elixir is totally cool with that. Just like we can use anything as items within a list, Elixir also lets us use anything as the keys and values within a map.

Now that we've established that, I want to introduce you to one more type of data within Elixir by way of a little code example. One common occurrence in Elixir code that you might see around is maps that look like this:

iex> person = %{
  name: "Izzy",
  age: "30ish",
  gender: "Female"
}

This is a short-hand way of writing:

iex> person = %{
  :name => "Izzy",
  :age => "30ish",
  :gender => "Female"
}

The keys in both these examples here are neither numbers, strings, lists or maps. But they look sorta like strings, right? So what are they? These keys are called atoms. They're a type of data in Elixir that is commonly used to represent names of things, like in this case. They're a simple name, and nothing more. We'll see a lot more of them as we go through this book, and so it's helpful to know what they are now.

Just as we were able to access values associated with strings and numbers, we can also access the values associated with atom keys by using the square brackets like this:

iex> person[:name]
"Izzy"

This is a little less typing than person["name"], and the way we define the map is a little shorter too, and so it is generally preferred to use atoms over strings for keys within maps. This is why you might see it a lot in other people's Elixir code too.

There's one more advantage to using atoms as keys over strings: we can use an even shorter syntax to read out the values:

iex> person.name
"Izzy"

This is a full three characters shorter than any other way we've seen to work with a map. Typing less is always a good thing. And the code looks neater to boot without all that pesky punctuation.

So from here on out, we'll be using mostly atoms for keys within maps just because it's less typing and makes our code a little cleaner to work with.

And now to come back around to collecting information on who is gathered here today. For this, we can use a list of maps and have those maps have keys that are atoms, and values that are strings:

iex> those_who_are_assembled = [
  ...> %{name: "Izzy", age: "30ish", gender: "Female"},
  ...> %{name: "The Author", gender: "Male", age: "30ish"},
  ...> %{name: "The Reader", gender: "Unknowable", age: "Unknowable"},
  ...> ]

Izzy excitedly disappears into the crowd again with this new map knowledge and starts collecting people's information again.

5. Funky functions

After about half an hour, Izzy re-appears from seemingly nowhere. Her computer screen glows with the information she has collected from the crowd. We're not quite sure how long she's been back for, but the moment our attention fixes on her, she speaks: "Okay, so you can represent different... kinds of things in Elixir — strings, numbers, lists, maps — but what is the point of doing that at all? What can you do with them? Do I need to write code to work with these different kinds of things all the time? Can the computer remember code too?"

Well Izzy, that was a very clever and completely unintentional segue into this next part of the book. Thank you for doing that.

Yes, in fact you can tell the computer to remember some code too. This saves a lot of typing. Crazy amounts of the stuff. You'll get years of your life back!

Ok, enough chit-chat. Let's look at one way we can use to make the computer remember some code:

iex> greeting = fn (place) -> "Hello, #{place}!" end
#Function<6.52032458/1 in :erl_eval.expr/5>

This is called a function and we can write whatever code we want the computer to remember for later, just like we could tell the computer to remember a string, a number, a list or a map. Elixir code is all about the functions, and now we understand why Wikipedia said it was a "functional" language. It didn't mean that it was functional in the sense that it is operational, but more so that it uses functions to get things done. You'll be seeing a lot of functions in Elixir code from here on out.

The fn tells the computer we're about to define a function -- because typing function each time would just be too much for us to bear -- and the defining-of-the-function doesn't stop until it gets to the end. Just like you wouldn't stop until you got to the end of something -- i.e. a book -- right?

The (place) here defines an argument for the function; think of it like a variable that is only available within this function and not the angry shouting matches that most normal arguments can devolve into. The -> tells the computer that we're done defining the arguments for the function, and anything after this but before the end is going to be the code to run.

function definition example
Figure 5.1: Function definition example

Once we hit enter at the end of this line, the computer gives us some output to indicate it has accepted our function. It's not the friendliest output, but at least it's something that tells us the computer has done something.

iex> greeting = fn (place) -> "Hello, #{place}!" end
#Function<6.52032458/1 in :erl_eval.expr/5>

We've assigned our function to the greeting variable and so that's what the computer will remember the function as. "How do we use this function?", Izzy asks keenly, suddenly impressed about the computer's ability to remember functions. I'm sure you're asking the same thing, dear reader.

Well, Izzy (and dearest reader), we need to use some new and exciting code that we've not seen yet:

iex> greeting.("World")
"Hello, World!"

This is how we make the function run. We run this function by putting a dot after its name. The brackets after this dot represent the argument for the function. This time that we call the function, place will be "World". But it doesn't always have to be "World". It can be anything you wish:

iex> greeting.("Mars")
"Hello, Mars!"
iex> greeting.("Narnia")
"Hello, Narnia!"
iex> greeting.("Motherland")
"Hello, Motherland!"

Each time we run the function here we give it a new value for place. We don't have to set place ourselves, as the function takes care of that. But what of our place variable from yesteryear (line 9 in iex)? Has that changed?

iex> place
"World"

The computer knows that the place from outside the function is different to the place inside the function, and so it keeps the two separate. The computer is smart enough to know that place on the outside may not exist and so it shouldn't rely on it being present. The function contains everything it needs to run inside itself.

Our functions so far have just taken a string and put it inside another string, but functions are capable of executing any code, so let's have a quick look at another function. This function will convert a temperature in the sensible celsius unit to the wacky Fahrenheit equivalent, using numbers instead of strings:

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

The mathematical equation to convert celsius to fahrenheit is to multiply by 1.8 and then add 32, which is exactly what we're doing in our function: we take c representing the number in celsius, multiply (*) it by 1.8, and then add (+) 32.

Let's run our function with some celsius temperatures:

iex> c_to_f.(20)
68.0
iex> c_to_f.(24)
75.2
iex> c_to_f.(40)
104.0

If you want to check these, plug these into the all-knowing Google using a search time like "20 celsius in fahrenheit" and Google will confirm these answers.

Multiple-argument functions

One more handy thing about functions is that they're not limited to just one argument. You can define a function that accepts as many arguments as you wish:

iex> greeting = fn (name, gender, age) ->
...>   "Hello, #{name}! I see you're #{gender} and you're #{age} years old."
...> end
#Function<18.52032458/3 in :erl_eval.expr/5>

This function has 3 arguments, and so when we run it we need to give it all three. To do that, we just separate them using a comma, similar to how we separated the items in a list earlier, just without the square brackets ([]) around these arguments.

iex> greeting.("Izzy", "Female", "30ish")
"Hello, Izzy! I see you're Female and you're 30ish years old!"

We can go nuts with the number of arguments that a function is defined with. However, we need to take a modicum of caution when running the functions. If we specify the wrong number of arguments, Elixir will tell us off with big red text:

iex> greeting.("Izzy")
** (BadArityError) #Function<18.52032458/3 in :erl_eval.expr/5>⏎
  with arity 3 called with 1 argument ("Izzy")

Oh no the computer is angry with us. Well, if the computer felt any emotion from brutal indifference it would probably be some version of angry, or at least disappointed. We've just made a happy little accident here by specifying 3 arguments. The computer is reprimanding us: telling us that we caused a BadArityError and that means that we had a "<function> with arity 3 called with 1 argument".

"What on earth is an 'arity'?", Izzy asks, clearly flummoxed (and perhaps a bit affronted) by the word. Unlike the computer, Izzy is not brutally indifferent when it comes to these things. After all, Izzy thought she was getting a handle on this Elixir thing.

Arity is a fancy computer term which means "arguments". This error is saying that while the greeting function is defined with 3 arguments ("with arity 3") we're only running ("calling") it with 1 argument. Helpfully, it tells us what arguments we tried to give the function. To avoid the computer reprimanding us we should make sure to call functions with the right number of arguments.

Capture operator

Elixir provides us with the fn construct so that we can write functions, and we've seen a few examples of that so far. There's one other construct you might see if you read other people's code, and it's called the capture operator and it goes like this:

iex> captured_greeting = &("Hello #{&1}!")

We must wrap the entire contents of the function with brackets, and then inside the brackets we can use &1 to capture the first argument passed to this function, &2 to capture the second and so on. For now, we're just capturing the one.

This function is functionally (ahem) equivalent to our first greeting function:

iex> greeting = fn (name) -> "Hello #{name}!" end

We can see this if we call both functions with the same argument:

iex> captured_greeting.("World")
"Hello World!"
iex> greeting.("World")
"Hello World!"

As I mentioned before, you can capture as many arguments as you wish. We can take our 3-argument greeting function from earlier:

iex> greeting = fn (name, gender, age) ->
  ...>   "Hello, #{name}! I see you're #{gender} and you're #{age} years old."
  ...> end

And we can write it in a slightly shorter way by using the capture operator:

iex> captured_greeting = &("Hello, #{&1}! I see you're #{&2} and you're #{&3} years old.")

Then we can call it with three arguments too:

iex> captured_greeting.("Izzy", "Female", "30ish")
"Hello, Izzy! I see you're Female and you're 30ish years old!"

Functions written with the fn construct and the capture operator behave identically. The only differences are that the capture operator syntax is shorter, and you refer to arguments by their position rather than a name.

Saving code for later

While it's all good and well to run code through the iex prompt, you may want to save some code to run later on. Maybe at this point you might have an idea of something to try out and you want to save it somewhere for later on because an iex prompt will only remember what you typed for as long as it is open. Once it's shut, that code is gone for good and you will need to type it all out again.

To save your Elixir code, you can create a new file in your favourite text editor, put in the code you want and then save it with a name like hello.exs. That .exs on the end symbolises that the file is an Elixir Script file.

To run the code inside the file, simply run elixir hello.exs. No need for a prompt now!

Exercises

  • Make a function which turns Fahrenheit temperatures into celsius.
  • 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.
  • Make a function which takes two maps with "age" keys in them and returns the average age.
  • Save any of these three solutions in their own file and run them through the elixir command-line tool.

6. Pattern matching

Back in Chapter 2 you saw that the equals sign (=) made the computer remember things.

iex> sentence = "A really long and complex ⏎
sentence we'd rather not repeat."

"A really long and complex ⏎
sentence we'd rather not repeat."

iex> score = 2 / 5 * 100
40

While this is indeed still true, the equals sign (=) can do more than just set one value at a time. (Izzy double-takes at the last sentence, while the crowd murmurs.) There's a hidden feature of Elixir that we haven't shown yet, and that feature is called pattern matching. You'll use this feature quite a lot when programming with Elixir — just as much as functions! — so we'll spend a while talking about it here in this chapter too.

Equals is not just for equality

The equals sign isn't just about assigning things to make the computer remember them, but it can also be used for matching things. You can think of it like the equals sign in mathematics, where the left-hand-side must equal (or "match") the right-hand-side for the equation to be valid.

For instance, if we tried to make 2 + 2 = 5, much like Nineteen Eighty Four's Party would want us to believe, Elixir would not have a bar of it:

iex> 5 = 2 + 2
** (MatchError) no match of right hand side value: 4

Unlike the famous Mr. Winston Smith, Elixir cannot ever be coerced into disbelieving reality. Here, Elixir is telling us that 2 + 2 is indeed not 5. In Elixir, the left-hand-side has to evaluate to the same as the right-hand-side. This will make the computer happy:

iex> 4 = 2 + 2
4

Similarly, having two identical strings on either side of the equals sign will also make the computer happy:

iex> "dog" = "dog"
"dog"

Let's do something more complex than having the same thing on both sides of the equals sign. Let's take a look at how we can pattern match on lists.

Pattern matching with lists

Let's say we have a list of all the people assembled here, like we had back at the end of Chapter 4:

iex> those_who_are_assembled = [
...> %{age: "30ish", gender: "Female", name: "Izzy"},
...> %{age: "30ish", gender: "Male", name: "The Author"},
...> %{age: "56", gender: "Male", name: "Roberto"},
...> %{age: "38", gender: "Female", name: "Juliet"},
...> %{age: "21", gender: "Female", name: "Mary"},
...> %{age: "67", gender: "Female", name: "Bobalina"},
...> %{age: "54", gender: "Male", name: "Charlie"},
...> %{age: "10", gender: "Male", name: "Charlie (no relation)"},
...> ]

And let's also say that we wanted to grab first 3 people in this list, but then ignore the remainder -- given that the first three are clearly the most important people here. We can use some pattern matching on this list:

iex> [first, second, third | others] = those_who_are_assembled

With this code, we're telling Elixir to assign the first, second and third items from the list to the variables first, second and third. We're also telling it to assign the remainder of the list to the others variable, and we specify that by using the pipe symbol (|). In this code we've selected the first 3 items from the list, but we could also just select the very first one, or the first 5. It doesn't have to be exactly 3.

We can check what this has done exactly in iex by looking at the values of each of these variables:

iex> first
%{age: "30ish", gender: "Female", name: "Izzy"}
iex> second
%{age: "30ish", gender: "Male", name: "The Author"}
iex> third
%{age: "56", gender: "Male", name: "Roberto"}
iex> others
[%{age: "38", gender: "Female", name: "Juliet"}, ...]

"Does this mean that I could do the same for the others list to get the next 3 people?", Izzy asks. Yes it does mean that you can do that:

iex> [first, second, third | remainder] = others
[%{age: "38", gender: "Female", name: "Juliet"}, ...]

Now when we check the values of first, second and third they'll be the names of the next 3 people in the list:

iex> first
%{age: "38", gender: "Female", name: "Juliet"},
iex> second
%{age: "21", gender: "Female", name: "Mary"},
iex> third
%{age: "67", gender: "Female", name: "Bobalina"},

And our remainder variable will be the remaining names in the list, which are just the two Charlies:

iex> remainder
[
  %{age: "54", gender: "Male", name: "Charlie"},
  %{age: "10", gender: "Male", name: "Charlie (no relation)"},
]

If we now try to pull out the next 3 people in the list, Elixir won't be able to do that because there are only two names left in the remainder list. I've shortened the output in the below example, but I think you'll get the gist:

iex> [first, second, third | those_still_remaining] = remainder
** (MatchError) no match of right hand side value: ... ⏎
   [%{name: "Charlie"}, %{name: "Charlie (no relation)"}]

It's always a good idea to be careful here with pattern matching lists with the right number of expected items to avoid MatchErrors like these. Normally when we would work through each item of the list, we would do so not in groups of three, but one at a time.

We'll see two examples of working through each item in a list one-at-a-time, once in Chapter 9 and once within Chapter 10. We'll need to build up to those, so let's not worry too much about those yet.

That's enough about lists for now. We'll revisit pattern matching them a little later on in this book. Let's look at how we can work with maps.

Pattern matching with maps

Let's say that we have a map containing a person's information:

iex> person = %{name: "Izzy", age: "30ish"}
%{name: "Izzy", age: "30ish"}

We've seen before that we could pull out the value attached to name: or age: at whim by using this syntax:

iex> person.name
"Izzy"
iex> person.age
"30ish"

But what if we wanted to pull out both of these values at the same time? Or even in the shortest possible code? Well, for that we have this pattern matching thing that I've been banging on about for over a page. Let's take a look at how pattern matching can help get both the name and the age out of the person map.

iex> %{name: name, age: age} = person

"Hey, what gives? The left-hand-side here is clearly not the same as the right hand side!", cries Izzy. Yes, you're absolutely right. On the right hand side here we have a map which has a "name" key which points to a value of "Izzy", but on the left hand side that "name" key points to a value of name. This is a trick of pattern matching: the left-hand-side can be used to assign multiple variables; it doesn't have to match the right-hand-side exactly.

If we check the value of name and age here, we'll see that those values are the values from our map.

iex> name
"Izzy"
iex> age
"30ish"

Well, would you look at that? We were able to pull out these two values at the same time. The crowd cheers as if we've just performed a magic trick. Just wait until you see our next trick!

Let's look at our previous example, where we had maps inside of a list:

iex> those_who_are_assembled = [
...> %{age: "30ish", gender: "Female", name: "Izzy"},
...> %{gender: "Male", name: "The Author", age: "30ish"},
...> %{name: "The Reader", gender: "Unknowable", age: "Unknowable"},
...> ]

Let's say that we wanted to get the name of the first person from this list. We can combine both the list pattern matching and the map pattern matching together:

[first_person = %{name: first_name} | others] = those_who_are_assembled

Here we're matching the first item from the list, and putting the rest in an others variable. We're grabbing just the first person from the list of those who are assembled. Inside that match, we're assigning that first person to first_person. We're then expecting that the first item of this list to match the pattern of %{name: first_name}. This will set the variable of first_name to be the value of the first item's name key. Phew, that was a long description. Let's go and see what we now have in the console:

iex> first_person
%{age: "30ish", gender: "Female", name: "Izzy"}
iex> first_name
"Izzy"

This can be tricky to wrap your head around at first since there's a lot going on here. It might take a few tries to read it and understand. It did for me when I first read an example like this! Take your time, it's OK to not get it first try.

What we're doing here is pulling out 3 distinct values from this one single line:

Figure 6.1: Complex pattern matching within a list
  • The first_person variable, which contains all the information we have on Izzy in map form.
  • The first_name variable, which has stored the string "Izzy"
  • The others variable, which stored the remaining people in the list.

As you can see from this short example, pattern matching is very flexible and allows you to match more than one thing at a time, and also allows you to set more than one variable. Programmers often refer to this sort of thing as destructuring: you're looking into the structure of the data and then pulling out only the things you want.

Pattern matching can be used for even more things than picking out the keys of a map or the items out of a list. We can also use it inside of functions!

Pattern matching inside functions

We can use pattern matching inside our functions to make them respond differently depending on the arguments passed in. For instance, we could define a function which took the kind of road that we took; either the "high" road or the "low" road, and get it to respond differently depending on which was passed.

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
end

When we call this function with the "high" argument, that argument will match the first function line here, and "You take the high road" will be returned. Similarly, when we give it "low" it will return "I'll take the low road! (and I'll get there before you)". Each line inside the function here is called a clause. We could keep talking about the theory behind this, or we could actually try it in our iex prompt:

iex> road.("high")
"You take the high road!"
iex> road.("low")
"I'll take the low road! (and I'll get there before you)"

This works because of how we've defined the road function. In that function, we've defined two separate function clauses. The first function clause says that when the argument is "high" then the function should output the line about the "high road". The second function clause says that when we supply the "low" argument then it should output the line about the "low road".

Think of it like this: Elixir is pattern matching the value of the argument against the clauses of the function. If Elixir can see that the argument is equal to "high" then it will use the first function. If it isn't equal to "high", then Elixir will try matching against "low". If the argument is "low" then the second clause will be used.

But what happens if it's neither? We can find out with a touch of experimentation in our iex prompt:

iex> road.("middle")
** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/1

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

        # 1
        "middle"

Elixir here is showing us an error that we've never seen before: a FunctionClauseError. This error happens when we call a function giving it arguments that it doesn't expect. In the case of our road function, it's expecting either "high" or "low", but we gave it "middle". Both clauses of the function don't match the value of "middle", and so we see this error. To be extra helpful, Elixir is showing us the argument that we passed here.

Matching on strings in functions is great, but as we saw earlier with the equals sign (=) we can match on more than just strings.

Matching maps inside functions

We can also match on the keys contained within a map and get the code to act differently depending on what keys are present. Let's take our greeting function from Chapter 5 and modify it slightly so that behaves differently depending on what kind of map we pass it:

iex> greeting = fn
  %{name: name} -> "Hello, #{name}!"
  %{} -> "Hello, Anonymous Stranger!"
end

"Oooh that's fancy! What is the empty map is for?", Izzy asks. Soon, Izzy. Soon. Let's see what happens if we call this greeting function with a map which has a "name" key:

iex> greeting.(%{name: "Izzy"})
"Hello, Izzy!"

Here, the first function clause is matching because the map we're supplying contains a key which is "name", and that's what the first function clause (highlighted below in green) expects too: a map which has a key called "name". So when we call this function with this map with a "name" key, we see the string "Hello, Izzy!" output from the function.

iex> greeting = fn
  %{name: name} -> "Hello, #{name}!"
  %{} -> "Hello, Anonymous Stranger!"
end

Now let's see what happens if we call this function with an empty map:

iex> greeting.(%{})
"Hello, Anonymous Stranger!"

Elixir is still acting as we would expect it to: we supplied an empty map and the second function clause matches an empty map, and so that's the clause that will be used here instead.

iex> greeting = fn
  %{name: name} -> "Hello, #{name}!"
  %{} -> "Hello, Anonymous Stranger!"
end

Ok, so what would you expect to happen here if you supplied neither a map with a "name" key or an empty map, but a map with a different key in it? "Based on the string test, I would expect it to fail with a FunctionClauseError!", Izzy proudly proclaims. Looks like someone has been paying attention. Dear Izzy, that is what I expected to happen too when I learned Elixir. However, maps are matched differently to strings in Elixir. Let's look:

iex> greeting.(%{age: "30ish"})
"Hello, Anonymous Stranger!"

The greeting function still displays "Hello, Anonymous Stranger!" So what gives here?

Well, in Elixir when you match two maps together it will always match on subset of the map. Let's take a look using our trusty equals sign (=) again:

iex> %{} = %{name: "Izzy"}
%{"name" => "Izzy"}

Just like in the second clause from the function above, we're comparing an empty map on the left-hand-side to a map from the right hand side. When pattern matching maps like this, it's helpful to think of the left-hand-side showing the keys that are absolutely required for the match to work. The right-hand-side must contain the same keys as the left-hand-side, but the right-hand-side can contain more keys than what's on the left.

This match will succeed because there are no keys required by the left-hand-side of this match. This story is different if we've got a map on the left-hand-side with keys, as we've seen before with the first clause of our greeting function:

iex> greeting = fn
  %{name: name} -> "Hello, #{name}!"
  %{} -> "Hello, Anonymous Stranger!"
end

In the first clause's case, it will only match if the argument passed to the greeting function is a map which contains a "name" key; this key is required by the match. If the map does not contain a "name" key then this clause will not match. The second clause matches any map, and so that is the clause that will be used for any map not containing a "name" key.

Matching anything

Now one more thing I wanted to show you is how to avoid those pesky FunctionClauseErrors when all the function clauses inside a function don't match. Let's take a look at the road function again:

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
end

If the argument supplied to this function is neither "high" nor "low" then Elixir shows us a FunctionClauseError:

iex> road.("middle")
** (FunctionClauseError) no function clause matching in ⏎
:erl_eval."-inside-an-interpreted-fun-"/1

What if instead of this error we could get the function to tell whoever was running it that they have to supply either "high" or "low" as the argument? Elixir allows us to do this by using an underscore (_) as the argument in a new function clause:

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
  _ -> "Take the 'high' road or the 'low' road, thanks!"
end

This underscore (_) matches anything and is often used in cases like this where we want to show a message if no other function clause matches. Let's see this in action:

iex> road.("middle")
"Take the 'high' road or the 'low' road, thanks!"

This underscore doesn't just match strings, but it will match any other argument that we can pass to the function:

iex> road.(%{})
"Take the 'high' road or the 'low' road, thanks!"
iex> road.(["high", "low"])
"Take the 'high' road or the 'low' road, thanks!"

What this does is skip the first two clauses of the function, and so the third clause will be used instead:

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
  _ -> "Take the 'high' road or the 'low' road, thanks!"
end

This helps keep people in line when it comes to providing the right arguments to the function, without Elixir blowing up in their face when they provide the wrong ones.

Exercises

  • 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?

7. Part 2: Recap

Elixir would be pretty hard to do if you had to write all the code for your programs by yourself. We have already seen that if you want to add, subtract, multiply or divide numbers, Elixir has your back:

iex> 2 + 4
6
iex> 3 - 6
-3
iex> 4 * 12345
49380
iex> 1234 / 4
308.5

We also saw that it can run code inside of some other code, through string interpolation:

iex> place = "World"
iex> "Hello #{place}!"
"Hello, World!"

We saw that it can handle lists:

iex> ["chicken", "beef", "and so on"]
["chicken", "beef", "and so on"]

And it can also handle maps:

iex> person = %{name: "Izzy", age: "30ish", gender: "Female"}

We also saw that it could remember functions for us:

iex> greeting = fn (name) -> "Hello, #{name}!" end
#Function<6.52032458/1 in :erl_eval.expr/5>

And in the last chapter we saw that we could do pattern matching:

iex> %{name: name, age: age} = %{name: "Izzy", age: "30ish"}
%{name: "Izzy", age: "30ish"}
iex> name
"Izzy"
iex> age
"30ish"

But what if I told you that it could do more than this simple math, string interpolation, remembering functions and pattern matching? What if I told you that Elixir already provided some functions that used all of the above things to aid you in building your programs if only you had asked to use them?

Well, all you need to do is ask and Elixir will provide.

Part Three: Building on the foundations

8. Working with strings, input and output

In this chapter we'll go through some of the built-in functions that Elixir has. We'll be focussing specifically on strings, input and output. Elixir has some very handy functions already available to use and we'll see just a small taster of that here.

Working with strings

We'll start by looking at the functions to work with strings. Strings are where we started back in Chapter 1, so it only makes sense to start with them here, too.

Reversing a string

Have you ever wanted to reverse a sentence, but didn't want to type all the different characters yourself? Elixir has a handy function for this called String.reverse. Here it is in action:

iex> String.reverse("reverse this")
"siht esrever"

Izzy elicits a noticeable "wooooowww, that's so cool" at this. "What else does Elixir have?", she asks quickly. We'll get to that, Izzy. Let's take the time now to understand what's happening here first.

The functions that Elixir provides are separated into something akin to kitchen drawers or toolboxes, called modules. Whereas in your top kitchen drawer you might have forks, knives, sporks, and spoons (like every sensible person's kitchen does), and in another you might have measuring cups, and in another tea towels, in Elixir the functions to work with the different kinds of data are separated into different modules. This makes finding functions to work with particular kinds of data in Elixir very easy.

Here, we're using the reverse function from the String module ("drawer" / "toolbox"). We know that this is a module because its first letter is upper-case. We're passing this String.reverse function one argument, which is a string "reverse this". This function takes the string and flips it around, producing the reversed string: "siht esrever".

Note here how we don't need to put a dot between the function name and its arguments, like we had to do with the functions we defined ourselves. You don't need to do this when you're running a function from a module. You only need the dot if you've defined the function and assigned it to a variable. For instance, with our old greeting function:

iex> greeting = fn (place) -> "Hello, #{place}!" end
#Function<6.52032458/1 in :erl_eval.expr/5>
greeting.("world")

When calling the String.reverse function, Elixir knows that it's a function because of that String. prefix. We don't need a dot right after the function name:

iex> String.reverse("reverse this")
"siht esrever"

Splitting a string

What about if we wanted to split a string into its individual words? For that, we can use the String.split function:

iex> String.split("split my string into pieces")
["split", "my", "string", "into", "pieces"]

We now have a list of the words in the string. We'll see what we could do with such a list in the next chapter. For now, let's look at what else is in this String module.

Replacing parts of a string

What about if we wanted to replace all the occurrences of a particular letter in a string with another one? For that, Elixir has the String.replace function:

iex> String.replace("moo", "o", "e")
"mee"

This function takes three arguments:

  1. The string we want to modify
  2. The part of the string we want to change
  3. What we want it to change to

The way we've used the function here means that it will change all the "o"s in the string into "e"s.

It's worth noting that we can change more than single character at a time too:

iex> String.replace("the cow jumped over the moon", "oo", "ee")
"the cow jumped over the meen"

Notice here that it didn't change the "o" in the word "cow" or the other "o" in the word "over". This is because we told the function to look for two "o"s in a row.

Making all the letters of a string uppercase

What about if we wanted to make the computer turn a string into its shouty variant? We can use upcase for this:

iex> String.upcase("not so quiet any more")
"NOT SO QUIET ANY MORE"

Making all the letters of a string lowercase

At the opposite end of that particular spectrum, there is downcase:

iex> String.downcase("LOUD TO QUIET")
"loud to quiet"

So as you can see, the String module has some helpful functions that can help us whenever we need to split a string, turn it all into upper / lower ("down") case. There's plenty more functions in the String module, and we'll see some of those in due time.

Input and output

Input and output are two fundamental things that you'll work with while programming. Programming is all about taking some data as an input and turning it into some form of an output. We've seen this multiple times already with the functions we've defined and used throughout this book. For instance, in that String.downcase function just above, the string "LOUD TO QUIET" is the input, and the "loud to quiet" generated by the method is the output.

What we'll cover in this section is getting some input from a different source: a new prompt. We'll prompt the user for their name and then we will use whatever they enter to output a message containing that input.

Making our own prompts

Let's say that we wanted to prompt people for their names and we wanted to prompt them in a way that meant that they didn't have to read Joy of Elixir to understand that strings had to be wrapped in double quotes and that they had to enter their input into an iex prompt.

Fortunately for us, Elixir has a module that provides us a function to do just this. That module is called IO (Input / Output) and the function is called gets. The name gets means "get string" and it will allow us to do exactly that. Let's see this function in practice:

iex> name = IO.gets "What is your name?"
What is your name?

"Hey what happened to our iex> prompt?", Izzy asks. Good question! We're using gets and passing it a string. This string then becomes a new prompt. This prompt is asking us for our name. Let's type in our name and press enter:

iex> name = IO.gets "What is your name?"
What is your name?The Reader
"The Reader\n"

Ok, so there's some output here. But what does it mean? If we check our name variable's contents we'll see that it contains this "The Reader\n" string.

iex> name
"The Reader\n"

Izzy continues asking great questions: "What's that \n on the end?". That is a new line character and tells the computer that we pressed enter. While the IO.gets function stopped prompting us after we pressed enter, it still kept the enter in case we wanted it too. In this particular case we don't really want that character. We can get rid of it by using another function from the String module, called trim.

iex> name = String.trim(name)
"The Reader"

That's much better! Now we have our name without that pesky new line character suffixed. What String.trim does is remove all the extra spacing from the end of a string, giving us just the important parts.

Taking input and making it output

We've now got some input, but what's the point of taking input if you're not going to do anything with it? So let's do something with it! What we'll do with this input is to output a greeting message.

Let's deviate here from using the iex prompt and instead write our code inside one of those Elixir Script (.exs) files we mentioned back at the end of Chapter 5. Let's call this file greet.exs and put this content inside of it:

name = IO.gets "What is your name? "
age = IO.gets "And what is your age? "
IO.puts "Hello, #{String.trim(name)}! You're #{String.trim(age)}? That's so old!"

Well that's a bit sneaky of that IO.puts to just appear out of nowhere! Just like gets means "get string", puts means "put string". This function will generate some output when our script runs. If we didn't have this IO.puts here, our program would only take input, and it would not generate any output.

In this function we're interpolating the output of the String.trim function twice. Remember: we're doing this to remove the new line character (\n) from the result of the IO.gets calls.

There's some more new syntax that we've never seen before either. We've seen that we could interpolate variables into strings, but we've never seen that we could call functions while interpolating too. It's absolutely something you can do in Elixir. When interpolating inside a string you can put any code inside the interpolation brackets (#{}) — but as a general rule-of-thumb it's good to keep this interpolated code as short and simple as possible. Normally, you would only interpolate variables. We're making a small exception here to interpolate a function instead.

Let's run our greet.exs script now. First, we'll need to stop our iex prompt, which we can do by pressing Ctrl+C twice. Then we can run the script with this command:

elixir greet.exs

Here's what we'll see initially:

What is your name?

The script is prompting us for our name and it is doing that because the first line of code in that script is running the IO.gets function. Let's enter our name again and press enter.

What is your name? Reader
And what is your age?

This little script is now prompting us for our age. This is because the second line is calling another IO.gets. Let's enter our age and then press enter again,

What is your name? Reader
And what is your age? 30ish
Hello, Reader! You're 30ish? That's so old!

Our script gets to the third and final line, where it runs the IO.puts function and outputs its little teasing message. Apparently being 30ish is old! Kids these days have no respect.

This is just a small example of what we can do with IO.gets and IO.puts. We could use any number of IO.gets and IO.puts function calls to build up a program that took user input and generated some output from it.

The unchanging world

Now's a good time as any to introduce to you to another feature of Elixir, called immutability. Immutability is another one of those big computer science-y fancy words which is used to describe things that do not change over time. "Do not change over time" made a whole load more sense to me than "immutability" when I first heard the word, to be perfectly honest.

We're talking about it in this chapter because nearly everything you've worked with so far in Elixir is immutable; unchanging and unchangeable. Most things in Elixir cannot be changed, modified, altered, messed with or any other synonym for those terms, and the way we talk about this particular attribute for these things is to say that these things are immutable.

When we've called a function such as String.downcase or String.upcase, we pass these functions strings as arguments and then we get back another string from the function; two completely different strings. All functions in Elixir behave this way: they cannot modify what they're given. Let's look at a quick example:

iex> sentence = "perfectly normal sentence"
"perfectly normal sentence"
iex> upcased_sentence = String.upcase(sentence)
"PERFECTLY NORMAL SENTENCE"
iex> sentence
"perfectly normal sentence"

By running String.upcase on the sentence, it doesn't change what the sentence is -- it's still exactly as we defined it. The data stored in the sentence variable is immutable. The only way we could change what is in that variable is if we re-assigned that variable:

iex> sentence = "another, even more perfectly normal sentence"
"another, even more perfectly normal sentence"

It's important to know in Elixir that calling functions on data will never change that data. What will happen instead is that we'll get back some new... thing as a result of that function. This will become more apparent the more Elixir code you write, but I thought it best to mention it here before you get too deep and someone mentions that big fancy word — "immutable". Now you're, as they say, "in the know" too.

Exercises

  • 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.
  • 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.

We've done a lot of work with strings so far in this chapter. Let's look at lists and maps again in the next chapter and the built-in functions that we can use with them.

9. Working with lists

In this chapter, we'll cover some functions that Elixir has helpfully got built-in to the language to help us work with lists.

Just like there is a String module for working with strings, there is also a List module for working with lists. This module includes functions like first that will tell you what the first item in a list is:

List.first([1, 2, 3, 4])
1

And it also lets you figure out what the last item is too:

List.last([1, 2, 3, 4])
4

We looked earlier at how we could reverse a string, but how about if we wanted to reverse something else, like this list?

iex> animals_or_derivatives_of_animals = ["cat", "dog", "cow", "turducken"]

We would think that just like first and last that there should be a reverse too! We might think this because Elixir provides us a way to reverse strings -- with String.reverse("string") -- so why not lists too? Let's try it out:

iex> List.reverse(animals_or_derivatives_of_animals)
List.reverse(animals_or_derivatives_of_animals)
** (UndefinedFunctionError) function List.reverse/1 is undefined or private
    (elixir) List.reverse(["cat", "dog", "cow", "turducken"])

Uh oh, there's that red text again. It seems like that this function doesn't exist, even though we thought it did. We are lacking the superpower of being able to will functions into being at this current point in time, so we'll have to do some sleuthing to figure out why it's missing.

The computer is telling us that Elixir doesn't know about a function called List.reverse, or the function is "private". The computer (slyly) won't tell us which one of non-existence or privateness it is, but we'll assume the first case here: that the function is undefined. (We'll come back to private functions a little later on in the book.)

Ok, so we've talked about what /1 means (Izzy is now deep in thought), so let's talk about why List.reverse/1 doesn't exist.

Thank you Reader, but our princess function is in another castle module

The short version of why the List.reverse/1 function doesn't exist is because it lives in a separate module; it exists in the Enum module. The name Enum is short for Enumerable, and it's done that way because nobody has the time to write out Enumerable correctly every single time. Even as this book's author I have a hard time spelling it correctly each time! Thank goodness for autocomplete.

"What on earth is an enumerable?", Izzy cries out. Hold your horses, Izzy. We're getting to that.

Lists are a type of data in Elixir called an enumerable. Maps are also enumerables. This means that they can be enumerated through; which means that you can do something with each item in the enumerable object (a list or a map). For instance, if we were to write out our list on a piece of paper, it might look something like this:

  • Cat
  • Dog
  • Cow
  • Turducken

It's possible to write each item from the list separately from the other items in the list. Because we're able to do this we can safely say that this list is enumerable. We could try to do the same thing for a number (like 1,354), but it wouldn't make sense:

  • 1
  • 3
  • 5
  • 4

Numbers are not enumerable because it doesn't make sense for them to be written like this, unlike our list.

Similar to this, we could enumerate through a map. If we were to take one of our maps from earlier...

%{name: "Izzy", age: "30ish", gender: "Female"}

...and write each key and value pair down, they may look something like this:

Name
Izzy
Age
30ish
Gender
Female

Again, it makes sense for a map to be an enumerable because you can enumerate through each of the key and value pairs in the map.

And now for the big reveal

We need to keep in mind that when we're working with enumerable things in Elixir the function that we need might live elsewhere. While there is most certainly a List and a Map module (which we'll see in the next chapter) -- sometimes, we need to look in the Enum module too for the function we want.

We tried looking in the List module to find the reverse/1 function so that we could turn our list around but it wasn't there. We found out a short while ago that the function is actually within the Enum module, so let's try using that function. Before that, let's get our list in Elixir form again. It's been a while since we've seen it that way:

iex> animals_or_derivatives_of_animals = ["cat", "dog", "cow", "turducken"]

Since we now know that lists are enumerables, and that the List.reverse/1 function doesn't exist but we also (now) know that there's an Enum module to work with this sort of thing, we can probably guess that there's going to be an Enum.reverse/1 function. Let's try it and see:

iex> Enum.reverse(animals_or_derivatives_of_animals)
  ["turducken", "cow", "dog", "cat"]

Hooray! We were able to reverse our list.

Izzy's features relax from intense concentration to a more neutral setting and she asks, "Hey, you mentioned before you could enumerate through a list or a map, but you didn't show an example of that. What gives?" You're absolutely right, Izzy. I was too distracted with explaining why List.reverse/1 didn't exist to explain how to enumerate through an enumerable. I'm glad that someone is paying attention.

Let's all now take a look at how to do that before we move onto other functions. We've talked about enumerables briefly and it would be a shame to stop so early when you can do so much more with them than reverse them.

Enumerating the enumerables

To appease Izzy (and the masses that she leads) yet again, we're going to need to look at how to enumerate through enumerables. What this means is that we're going to get an enumerable (a list or a map) and we're going to go through each of the enumerable's items and do something with them.

Let's start with a list. How about a list of the world's top five most liveable cities, just for something different?

iex> cities = ["vienna", "melbourne", "osaka", "calgary", "sydney"]

The simplest way of enumerating through a list in Elixir is to use the Enum.each/2 function. Remember that the /2 here means that the function takes two arguments. Those two arguments are:

  1. The thing we want to enumerate over; and
  2. a function to run for each of the items.

Let's take a look at an example:

iex> Enum.each(cities, &IO.puts/1)

The first argument here is our list of cities. The second argument is the function that we want to run for each item in this list: specifically the built-in IO.puts/1 function. The & before this function name is referred to as the "capture operator". We saw this back in Chapter 5, but we were using it to build our own functions then:

iex> greeting = &("Hello #{&1}")

The & has another use, and that is to capture functions from modules so that we can use them later. In this example, we're capturing IO.puts/1 so that Enum.each/2 can call that function on each of the items in the cities list.

Let's take a look at what this combination of Enum.each, cities and &IO.puts/1 does:

iex> Enum.each(cities, &IO.puts/1)
vienna
melbourne
osaka
calgary
sydney
:ok

This particular combination of those three things has made Elixir output each city on a single line in our iex prompt. Then there's one more thing: that little blue :ok at the end of our output. That's a little atom value and it indicates to us that the iterating with Enum.each/2 worked successfully. We saw atoms like this used for keys in maps in previous chapters, but they can also be used in Elixir code like this too.

Let's take a closer look at what this Enum.each/2 code is doing. Specifically: what &IO.puts/1 is doing. You know in Elixir that functions can take arguments and you might have in your head that those arguments could be strings, lists or maps because this is what we've seen done previously. But function arguments can also be other functions. This is what's happening here when we're using &IO.puts/1. We're passing that function as an argument to Enum.each/2.

To get an idea of how Enum.each/2 is able to take the IO.puts/1 function and do something with it, we can use this code in the iex prompt:

iex> puts = &IO.puts/1
puts.("melbourne")
"melbourne"
:ok

With this code, we've assigned the IO.puts/1 function to a variable called puts. This is similar to what's going on inside the Enum.each/2 function: whatever function we pass as the second argument will be stored with a particular name. Then Enum.each/2 goes through each of the items and calls the provided function on it, just like we've done above.

"That last line looks exactly like how we ran functions back in Chapter 5", Izzy exclaims. Yes, Izzy! That is exactly right. Back in that chapter we defined a greeting function like this:

iex> greeting = fn (place) -> "Hello, #{place}!" end
#Function<6.52032458/1 in :erl_eval.expr/5>

We then made Elixir run this function using this syntax:

iex> greeting.("World")
"Hello, World!"

What we're doing different here with our puts = &IO.puts/1 is that we're using an inbuilt function rather than constructing one of our own. We can do this with any inbuilt function we please. Here's another example, using String.capitalize/1:

iex> cap = &String.capitalize/1
cap.("melbourne")
"Melbourne"

Here, the String.capitalize/1 function takes a string and turns the first letter into a capital letter. In this code example, we're making it so that we can call the String.capitalize/1 as if it were a function called cap. However, if we wanted to capitalise the names of our city using the String.capitalize/1 function with Enum.each/2 it wouldn't do very much:

iex> Enum.each(cities, &String.capitalize/1)
:ok

Nothing is output here besides :ok because we're not telling Elixir to output anything. It will dutifully run the function String.capitalize/1 for each item in the list, and then just as dutifully "forget" to tell us anything about the result.

If we want to see what the result would be from evaluating String.capitalize/1 for each item in the list, we're going to have to use a different function.

Map, but not the kind that you know already

The names of these cities should be capitalized because they are proper nouns, but whoever created this list neglected to capitalize them. Oops! What we should have is a list with proper capitalization:

["Vienna", "Melbourne", "Osaka", "Calgary", "Sydney"]

We tried using Enum.each/2 to give us this list of capitalized cities, but that just told us :ok and it didn't give us back a list of capitalized city names.

We're going to need a function that goes through each item in the list and applies String.capitalize/1, and then shows us the result of each application of that function. Something that would turn our existing cities list into a list like this one:

iex> cities = [
    String.capitalize("vienna"),
    String.capitalize("melbourne"),
    String.capitalize("osaka"),
    String.capitalize("calgary"),
    String.capitalize("sydney"),
  ]
["Vienna", "Melbourne", "Osaka", "Calgary","Sydney"]

But since we've already got a list we're going to have to do something to that list to turn it into what we want. That something is to use another function from the Enum module called Enum.map/2. This map function is different from the map kind of data (%{name: "Izzy"}), in that the function-called-map will take the specified enumerable and another function as arguments, then enumerate through each item in the list and run that other function for each item, and then return the result of each function run.

Enough jibber-jabber. Let's see this in actual practice:

iex> Enum.map(cities, &String.capitalize/1)
["Vienna", "Melbourne", "Osaka", "Calgary","Sydney"]

Hooray! Each of our cities now has a correctly capitalized name. What's happened here is that we've told the map function that we want it to run another function — that'd be the String.capitalize/1 function — on each item of the cities list. This is how we've been able to go from our existing list with non-capitalized city names to a new list with capitalized names.

BYO functions

One more quick example. When using Enum.each/2 or Enum.map/2 we don't necessarily need to pass a built-in function as the second argument. We could choose to pass our own function instead. Let's say that we had a list of numbers like this:

iex> numbers = [4, 8, 15, 16, 23, 42]

And then we wanted to multiply each number by a different number, let's say 2. We're lazy and we want the computer to do the heavy lifting, and so we can pass Enum.map/2 a function of our own design to achieve this goal:

iex> Enum.map(numbers, fn (number) -> number * 2 end)
[8, 16, 30, 32, 46, 84]

With our own function here, we're taking each element of the list — represented inside the function as the variable number — and then multiplying it by 2. When Enum.map/2 has finished going through the list, it outputs a new list showing us the result of running that function on all items in the list.

Reducing an enumerable

We've now looked at how to use two functions from Enum, each/2 and map/2. These are two of the most commonly used functions from this module, and that's why we looked at them. But there's a third function that I'd like to tell you about too: Enum.reduce/2. It's a very helpful function!

Enum.reduce/2 allows us to iterate through a list and gradually apply a function to each element in that list to get a final value. Think of it like when you're cooking: you're reducing all the ingredients into a meal. You don't think of a spaghetti bolognese as it being a combination of onion, garlic, beef mince, tomatoes, herbs and spaghetti. You think of it as a complete meal. When we want to combine values in Elixir to produce one final value, we would use Enum.reduce/2. Let's take a look at it now.

We might want Elixir to tell us what the average of a list of numbers is. For instance, we might want to calculate the average of some scores, say for a test. Hey look, here are some handy score numbers now (out of 100):

iex> scores = [74, 79, 89, 32, 79, 70, 32, 69, 76, 73, 88, 73, 82, 31]

I wanted to keep writing numbers above but 14 numbers just felt right for some reason. It looks like most people in this test did particularly well. Good on them! A few didn't do so great and only scored 32 and one scored 31. Anyway, let's not look too closely at the data. What we're trying to do is to calculate the average for these scores and to do that we might try to sum these numbers up ourselves on a piece of paper

Figure 9.1: Adding together numbers manually
Or we might try using a calculator instead, as that's less error-prone. The way that we would do this calculation on a calculator is exactly as above: we would add each number up, one at a time, to calculate the sum. Our input into the calculator would be this:

74 + 79 + 89 + 32 + 79 + 70 + 32 + 69 + 76 + 73 + 88 + 73 + 82 + 31

The calculator would then tell us that the sum of these numbers is 947, as our on-paper working out should've shown us too. Go ahead and check this in your iex console too if you'd like; after all, it's just a super-powered calculator.

Once we have the sum, then we need to divide that sum by the amount of numbers that we summed in order to get the average. Here, we have 14 numbers. So the average is 947 / 14, which a calculator or the iex prompt should both tell you: 67.64285714285714. The calculator may not have as many decimal places. If we wanted to write this whole operation in Elixir code, it would be very simple:

iex> (74 + 79 + 89 + 32 + 79 + 70 + 32 + 69 + 76 + 73 + 88 + 73 + 82 + 31) / 14

We sum up all the numbers, then divide it by how many scores we had. Easy! It's easy here because there are only 14 numbers. But what if we had scores from hundreds of people? We wouldn't want to enter all this into our calculator or the iex prompt, now would we? "No way!", contributes Izzy. Exactly! So let's look at how we could use Enum.reduce/2 to do the summing up for us, to save us having to work it out all ourselves.

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)

The reduce/2 function here does exactly the same calculations as our earlier prompt: it returns 947. It does this by taking the first number, then it adds the second, third, forth and so on to get the sum. How does it do this? Well, let's walk through what this function is doing.

First, we pass reduce/2 the list of scores that we want to sum up. Then we pass it another function which takes two arguments: score and sum. Inside this function, we add these two arguments together. But what values do score and sum have inside this function? One easy way to figure that out is to get Elixir to output their values every time this function inside reduce/2 is used:

iex> Enum.reduce(scores, fn (score, sum) ->
  IO.puts(sum)
  IO.puts(score)
  IO.puts("---")
  sum + score
end)

Even though the order of the arguments inside the function are score and sum, we're outputting them here as sum and score for reasons that will become clear soon. When we run this new variant of our reduce/2 function, this will be the output:

74
79
---
153
89
---
242
32
---
274
79
---
353
70
---
423
32
---
455
69
---
524
76
---
600
73
---
673
88
---
761
73
---
834
82
---
916
31
---
947

The first number that we're outputting is the sum and the second number is the score. If we look at the first four lines of our output we'll see three numbers: 74, 79 and 153.

74
79
---
153

If we look back at our scores list definition, we can see that its first two numbers are 74 and 79.

iex> scores = [74, 79, ...]

So then where does 153 come from? The answer is easy: 74 + 79 = 153. So we can tell from the output here that our reduce/2 function is taking the first item from our list and making that the initial sum argument for our inner function. The second item then becomes the score argument. In the function, we're adding these two values together, and that results in 153. The reduce/2 function keeps going through the remaining numbers in the list, adding them one-at-a-time until we get the final result.

So that's the wonderful reduce/2 function, or at least one example of it. As you can see, it's very handy for when we want to combine (or reduce) several items into one item.

Summing up a list

It's at this point that I should probably mention one more function in the Enum toolbox called sum/1. This function takes a list and sums up all the items in it, just like our reduce/2 function. These two functions are identical in how they work:

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.sum(scores)
947
iex> Enum.reduce(scores, fn (score, sum) -> sum + score end)
947

So if you're wanting to sum up a list of numbers, it's better to use Enum.sum/1, than to use to Enum.reduce/2, just because it's less code. If you want to do another operation other than summing, like subtraction, multiplication, division and so on, then it's better to use Enum.reduce/2.

Exercises

  • 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"]
  • Use Enum.reduce/2 to multiply these numbers together: [5, 12, 9, 24, 9, 18]

10. Working with maps

In the last chapter we looked at some functions that we could use to work with lists. We looked at some functions from the List module, but we really spent most of the chapter on the Enum module. In this chapter, we'll spend some time talking about how to use Enum.each/2 and Enum.map/2 on maps instead of lists. Then we'll cover some functions from the Map module.

We spoke in that last chapter of using the Enum.each/2 function to work with maps:

Similar to this, we could enumerate through a map. If we were to take one of our maps from earlier...

%{name: "Izzy", age: "30ish", gender: "Female"}

...and write each key and value pair down, they may look something like this:

Name
Izzy
Age
30ish
Gender
Female

But we never got around to actually showing you how this was done! Well, enough dilly-dallying, here's an example:

iex> person = %{name: "Izzy", age: "30ish", gender: "Female"}
 %{age: "30ish", gender: "Female", name: "Izzy"}
iex> Enum.each(person, fn ({key, value}) -> IO.puts value end)

Before I can go any further, Izzy interrupts. "Hey, those curly braces inside the function passed to each are weird! It looks like a map but it's not a map because it doesn't have the % before it. And it's not a list because the curliness of the brackets; a list has square brackets. So what is it?". Izzy is calling out this code specifically:

fn ({key, value}) ...

The {key, value} here indicates a type of data in Elixir that we haven't seen before called a tuple. Tuples work like lists in that the order of the elements inside them matters, but you can't enumerate through them like you can with lists; said another way: tuples are not enumerable. Tuples are used to represent ordered, linked data. In this example, each tuple represents a key and its linked value.

In this function passed to each, a tuple is used instead of two function arguments (i.e. fn(key, value) ->) to indicate that the key and the value share a connection. This key and value are not represented as a list (i.e. fn([key, value]) ->) because of this connection. (Also: Elixir in its own "brain" thinks of maps as a list of tuples, and so this is another reason why they're tuples, and not lists!)

Let's take another look at how we used Enum.each/2 and what the output was:

iex> Enum.each(person, fn ({key, value}) -> IO.puts value end)
30ish
Female
Izzy
:ok

Our use of Enum.each/2 goes through each key and value, ignores the key and outputs the value with IO.puts/1. This is almost identical to our previous example of Enum.each/2, where we used a list of cities with the IO.puts/1 function:

iex> Enum.each(cities, &IO.puts/1)

In the list example, we were able to use &IO.puts/1. In the map example though, we had to pull out the value by using the tuple, and then we needed to use the expanded version of IO.puts/1 (IO.puts(value)) to output that value. Let's look at what happens if we don't use that expanded version.

iex> Enum.each(person, &IO.puts/1)
** (Protocol.UndefinedError) protocol String.Chars not implemented for {"age", "30ish"}

Uh oh, the computer is mad at us again. This cryptic error message is a complicated way of saying that IO.puts/1 doesn't know how to output a tuple. We just need to be careful that we pass IO.puts/1 only things it can handle. If we give something that IO.puts/1 can't handle, it won't know how to output it, and so it will show this error.

We've now looked at Enum.each/2 for maps, just like how we looked at it in the last chapter for lists. In that last chapter we also looked at Enum.map/2 and Enum.reduce/2 and so it's only fair that we cover them here too.

Mapping over maps

I wonder what map we could use this time. We've used the person map quite a few times already:

iex> person = %{name: "Izzy", age: "30ish", gender: "Female"}

We should try to use another map for this section. Hmmmm. How about a map with the days of the week and their expected temperatures? Here's what the week ahead looks like in Melbourne at this current point of writing:

%{
  "Monday" => 28,
  "Tuesday" => 29,
  "Wednesday" => 29,
  "Thursday" => 24,
  "Friday" => 16,
  "Saturday" => 16,
  "Sunday" => 20
}

These expected temperatures aren't just random numbers. That's just Melbourne for you. It just can't make up its mind. Now what if for our American friends we represented these numbers in their familiar — but wacky — fahrenheit? Otherwise they might read the numbers and think that Melbourne is a frigid hellscape, which it most certainly is not.

So we want to convert these temperatures from their celsius amounts into the fahrenheit equivalents, turning the map into this:

%{
  "Monday" => 82.4,
  "Tuesday" => 84.2,
  "Wednesday" => 84.2,
  "Thursday" => 75.2,
  "Friday" => 60.8,
  "Saturday" => 60.8,
  "Sunday" => 68
}

We know already how to convert these numbers if it was simply a list of those numbers, without the days of the week, like this:

[28, 29, 29, 24, 16, 16, 20]

We would use Enum.map/2 to run a function on each of these numbers to convert the numbers to fahrenheit (using that function that we saw back in Chapter 5:

iex> forecast = [28, 29, 29, 24, 16, 16, 20]
  [28, 29, 29, 24, 16, 16, 20]
  iex> Enum.map(forecast, fn (temp) -> temp * 1.8 + 32 end)
  [82.4, 84.2, 84.2, 75.2, 60.8, 60.8, 68.0]

So what if we just tried using Enum.map/2 on our forecast map instead of on a list? What would that look like? Let's see:

iex> forecast = %{
  "Monday" => 28,
  "Tuesday" => 29,
  "Wednesday" => 29,
  "Thursday" => 24,
  "Friday" => 16,
  "Saturday" => 16,
  "Sunday" => 20
}
%{"Friday" => 16, ...}
iex> Enum.map(forecast, fn ({day, temp}) -> {day, temp * 1.8 + 32} end)
[
  {"Friday", 60.8},
  {"Monday", 82.4},
  {"Saturday", 60.8},
  {"Sunday", 68.0},
  {"Thursday", 75.2},
  {"Tuesday", 84.2},
  {"Wednesday", 84.2}
]

"Hey, that's no map! That's a list!", Izzy exclaims, seemingly adapting a quote from a relatively niche space opera series. That's right, Izzy. When we call Enum.map/2 on either a list or a map we're going to get back a list regardless. This has to do with how Elixir thinks of maps: it thinks of them as a list of key-value tuples, rather than the friendly %{} syntax that we know and love. To get this data back into the format that we're familiar with, we're going to need to use another function from the Enum toolbox, called Enum.into/2.

Let's run our Enum.map/2 line again, but this time we'll assign the output to a variable.

iex> new_forecast = Enum.map(forecast, fn ({day, temp}) -> {day, temp * 1.8 + 32} end)
[
  {"Friday", 60.8},
  {"Monday", 82.4},
  {"Saturday", 60.8},
  {"Sunday", 68.0},
  {"Thursday", 75.2},
  {"Tuesday", 84.2},
  {"Wednesday", 84.2}
]

Then we can use Enum.into/2 to convert this list of tuples back into a map:

iex> Enum.into(new_forecast, %{})
%{"Friday" => 60.8, "Monday" => 82.4, "Saturday" => 60.8, "Sunday" => 68.0,
  "Thursday" => 75.2, "Tuesday" => 84.2, "Wednesday" => 84.2}

That's better! Our data is back into the shape of a map... but the days of the week have lost their ordering. But that's okay because we don't care about ordering in maps: we access values in our maps by their matching key, not by their position.

So here we've seen how to use the Enum.map/2 and Enum.into/2 functions to iterate through a map's elements to run a function on each of those elements.

However, there is a cleaner way of writing the above code and that's with the help of one of the best Elixir features: the pipe operator. Let's spend a little while looking at the pipe operator now.

The wonderful pipe operator

I've just shown you one way to use Enum.map/2 to go over a map's values and to apply a function to each of those values. Here it is again, in case you missed it:

iex> forecast = %{
  "Monday" => 28,
  "Tuesday" => 29,
  "Wednesday" => 29,
  "Thursday" => 24,
  "Friday" => 16,
  "Saturday" => 16,
  "Sunday" => 20
}
%{"Friday" => 16, ...}
iex> new_forecast = Enum.map(forecast, fn ({day, temp}) -> {day, temp * 1.8 + 32} end)
  [
    {"Friday", 60.8},
    {"Monday", 82.4},
    {"Saturday", 60.8},
    {"Sunday", 68.0},
    {"Thursday", 75.2},
    {"Tuesday", 84.2},
    {"Wednesday", 84.2}
  ]
iex> Enum.into(new_forecast, %{})
%{
  "Friday" => 60.8,
  "Monday" => 82.4,
  "Saturday" => 60.8,
  "Sunday" => 68.0,
  "Thursday" => 75.2,
  "Tuesday" => 84.2,
  "Wednesday" => 84.2
}

What we're doing here can be broken down into a few steps:

  1. We define a variable called forecast, which is a map containing the days of the week and their expected temperatures.
  2. We then take this forecast variable, and pass it as the first argument to Enum.map/2. The second argument passed to Enum.map/2 is a function which runs on each key-value pair in the map. This function takes the temperature -- the value -- and converts it to fahrenheit.
  3. We assign the result of this Enum.map/2 call to a variable called new_forecast, and then immediately pass that into Enum.into/2 as the first argument. The second argument given to Enum.into/2 is an empty map. This converts the output of Enum.map/2 -- a list of key-value tuples -- into a friendly map.

Elixir has a way of writing this code in a shorter way that I would like to show you now as I believe it can lead to shorter, cleaner Elixir code. We might find more uses for it as we go along too, and so this seems like a pretty good time to introduce this particular concept.

The way to write shorter code here is by using the pipe operator, |>. The pipe operator allows us to chain together actions within Elixir, passing the result of one function on to the next. It's called the pipe operator because it connects the functions together in a linear fashion, with data flowing from one function to the next; like water through a pipe. Unfortunately, unlike Tony Hawk's Pro Skater, we do not get thousands of points for chaining together sick combos. But we still do get the great sense of satisfaction when it all works together!

Let's look at a small example of the pipe operator using functions from the String module first before we use it on this forecast data.

iex> "hello pipe operator" |> String.upcase() |> String.reverse()
"ROTAREPO EPIP OLLEH"

We can think of the pipe operator as "and then". In the above example, we:

  • Have a string
  • and then we call String.upcase()
  • and then we call String.reverse()

Here's an illustrated version:

Pipe operator on strings
Figure 10.1: How data flows from one function to another

This code takes the string, and then passes it to String.upcase(). The result of that operation is "HELLO PIPE OPERATOR". Then, we take that uppercased string and pass it to String.reverse(), which will then turn it into "ROTAREPO EPIP OLLEH".

Without the pipe operator, we would have to write this code as:

iex> String.reverse(String.upcase("hello pipe operator"))

This code is harder to read and understand, because we have to read it from the inside out. With the pipe operator, we read the code as we read this sentence: from left to right. We can more easily understand the transformations our data goes through by using the pipe operator in Elixir.

Let's now use this pipe operator on our forecast data. Here's some code that uses the pipe operator ("and then") that is functionally identical to the code from the previous section, where we have a forecast in celsius and we convert it to fahrenheit:

%{
  "Monday" => 28,
  "Tuesday" => 29,
  "Wednesday" => 29,
  "Thursday" => 24,
  "Friday" => 16,
  "Saturday" => 16,
  "Sunday" => 20
}
|> Enum.map(fn ({day, temp}) -> {day, temp * 1.8 + 32} end)
|> Enum.into(%{})
|> IO.inspect

Go ahead and put this code in a file called forecast.exs and run it with elixir forecast.exs. This is what you'll see as the output:

%{
  "Friday" => 60.8,
  "Monday" => 82.4,
  "Saturday" => 60.8,
  "Sunday" => 68.0,
  "Thursday" => 75.2,
  "Tuesday" => 84.2,
  "Wednesday" => 84.2
}

I've put each key and value onto its own line, but you can see here that the output is the same as before in our iex console, and the code is neater to boot! "Amazing", utters Izzy. So what's happened here? Let's run through it:

  1. We define a map containing the days of the week and their expected temperatures. We don't assign this to a variable.
  2. And then we pipe this map using the pipe operator (|>) into Enum.map/2. This makes the map the first argument of Enum.map/2, so this removes the need to define a forecast variable and to pass it in as the first argument. The pipe operator gives the map to the function automatically. The only argument on Enum.map/2 here is just the function that we use to convert the temperature values. We still refer to this function as Enum.map/2, because it is taking in two arguments: the piped-in value and the function.
  3. And then we pipe the result of this Enum.map/2 call into Enum.into/2, and the result of Enum.map/2 becomes the first argument of Enum.into/2. The second argument to Enum.into/2 is still an empty map.
  4. Finally, we pipe the result of Enum.into/2 into a function we've not seen before called IO.inspect/1. This outputs the map in a human-friendly way. We're not using IO.puts/1 here because it only works with strings, and what we have at this point of the code after Enum.into/2 is a map, not a string. IO.inspect/1 will work with any form of data that we give it.

"Wow, that's a lot of text!", says Izzy, looking a little stunned. "Do you have an image that I could look at instead?". Sure I do!

a flow chart from a Map datum to Enum.map to Enum.into to IO.inspect
Figure 10.2: How data flows from one function to another

You can think of the pipe operator not only as an "and then" operator, but also like the stages of a waterfall -- or "datafall" -- with data flowing from one function to the next.

Each operation that we're doing in forecast.exs passes its result onto the next line, in a pipeline of operations. Elixir allows us to chain these operations together seamlessly with this pipe operator. Everything just flows from one line to the next until we reach the end. There is no need for storing data into temporary variables like new_forecast; you just simply pipe the output from one function to the next.

So that's the wonderful pipe operator! How handy!

We started this chapter by making some promises, though. We've already fulfilled one promise—we've talked about the Enum module's functions each/2 and map/2. But we also promised to talk about some functions from the Map module. Let's get into it. We might even find another excuse to use the pipe operator.

The marvellous Map module and its fantastic contraptions

Rightio, so we've spent a lot of the last few pages looking at two modules: the List and Enum module. It's time for a breath of fresh air -- time to break out of our Enum comfort zone that we've wiggled into and time to get uncomfortable again! Let's look at a brand-new-to-us module: the Map module.

If you want to do nearly anything with maps that doesn't involve enumerating over them, you would use this Map module. Let's look at a few common actions that you might take on a map.

Finding a key's value

In Chapter 4: Marvellous Maps we saw that we could fetch the value of a particular key by using square brackets.

iex> person = %{"gender" => "Female", "age" => "30ish", "name" => "Izzy"}
%{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}
iex> person["name"]
"Izzy"

We compared it to a skilltester reaching in to pluck out the relevant item:

skilltester

The Map module provides us another way to get that value out: Map.get/2.

iex> Map.get(person, "name")

Now this might not seem very useful. After all, why not just use the old fashioned way: person["name"]? Well, one place where this might come in handy is if you want to find a particular key in the middle of a pipe chain. Imagine you have a variable called get_attendees that gives you data that looks like this:

%{
  "people" => ["Izzy", "The Author"],
  "robots" => ["Roboto", "TARS"]
}

Then from this data you wanted to get the first person from the list of people. You could write this as:


  attendees = get_attendees()
  List.first(attendees["people"])
  

But then this code is confusing. The order looks like:

  1. Get list of attendees
  2. Get the first from that list...
  3. That list of attendees["people"]

Both Elixir and our brains need to interpret what's inside the brackets of List.first/1 first, then interpret List.first/1. That requires us to jump around a little in the code because the code is not executed in the order that you would read it.

If we were to use the pipe operator (|>) and Map.get/2, we could really clean this code up and make it read a lot better. Here we go:

get_attendees() |> Map.get("people") |> List.first

That's much neater and it reads in the exact order of operations:

  1. Get the attendees
  2. Then, get the value that the "people" key points to
  3. Then, run List.first/1 on that value

So Map.get/2 has its uses when pipe chaining. However, if you just wanted to access a value in a map regularly, I'd still say stick with code like people["name"].

I want to show you three more functions from Map which I think are as important to know as Map.get/2 and those are Map.put/3, Map.merge/2 and Map.delete/2.

put

The Map.put/3 function takes three arguments: a map, a key and a value. It will then return an updated version of the original map. How that map gets updated depends on what the key was: if it was an existing key, only the value for that key is updated. If it's a new key, then both the key and value are added. Let's look at some examples to get a clearer understanding.

You might want to use this function if you've got a map containing data that you want to update. For instance, like the data that we have on Izzy so far:

iex> izzy = %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"}

Recently, we've found out that Izzy is from Australia. That would explain the accent and the cork hat. We've also found out that Izzy is "40ish" rather than "30ish". This is surprising to us, considering her youthful visage. Based on this new information, we'll add a new key to our map called country and update the age key with the more correct value. We could just type it all out again:

iex> izzy = %{
  "name" => "Izzy",
  "age" => "40ish",
  "gender" => "Female",
  "country" => "Australia",
}

But that seems like an awful lot of typing just to update one key. Let's look at how we could update the age key and add in the country key by using Map.put/3:

iex> izzy = Map.put(izzy, "age", "40ish")
%{"age" => "40ish", "gender" => "Female", "name" => "Izzy"}

Ah that's so much better than writing the whole map from scratch! We can now update the "age" key without having to rewrite the whole map.

Now let's say that we wanted to update this age key and the country key at the same time. We can use Map.put/2 and the pipe operator to chain the two operations together:

izzy |> Map.put("age", "40ish") |> Map.put("country", "Australia")
%{"age" => "40ish", "country" => "Australia", "gender" => "Female", "name" => "Izzy"}

This time, we've updated the value under the age key, and we've also added a new key here called country. It's important to note here that the original izzy map isn't changed at all:

izzy
%{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}

This is because of that immutability thing we talked about a few chapters ago. Data in Elixir never changes unless we re-assign the variable it is associated with. In the above Map.put/3 code, izzy stays the same, even though the functions run. If we wanted to update the izzy variable with new data, we could re-assign that variable:

izzy = izzy |> Map.put("age", "40ish") |> Map.put("country", "Australia")
%{"age" => "40ish", "country" => "Australia", "gender" => "Female", "name" => "Izzy"}

From that point onwards, our code would know that Izzy is 40ish and from Australia. Phew.

How we've used Map.put/3 here is another great example of the pipe operator in action, but we can write the code in a shorter fashion with the next Map function: merge/2.

merge

Let's change the data that we have on Izzy back to what it was at the start of this chapter:

izzy = %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"}

This'll make it easier to show what Map.merge/2 does. Map.merge/2 takes two maps and merges them together. Let's take our last Map.put/3 example and rewrite it with Map.merge/2 to demonstrate this:

izzy = Map.merge(izzy, %{"age" => "40ish", "country" => "Australia"})
%{
  "age" => "40ish",
  "country" => "Australia",
  "gender" => "Female",
  "name" => "Izzy"
}

This results in the same map as our Map.put/3 example, except we only have to call one function once instead of twice. The Map.merge/2 function takes two maps as arguments, and then:

  • Takes the keys from the second map that are not present in the first map adds them to the first map
  • Overwrites keys in the first map that are present in the second map

In this example, it has replaced the "old" age value ("30ish") with the "new" one ("40ish"), and it has added a key called country to this map.

Pipe Merging

While we can use Map.merge/2 to combine two maps to update an existing map, there is also a shorter way, which we'll call pipe merging. The syntax goes like this:

iex> izzy = %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"}
%{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}
iex> %{izzy | "age" => "40ish", "name" => "Isadora"}
%{"age" => "40ish", "gender" => "Female", "name" => "Isadora"}

This syntax looks like a map, but it's split in two. On the left hand side, inside the map syntax, we have a variable which represents an existing map. On the right hand side, we have keys we would like to update in that map, along with their new values. As we can see, the map's "age" and "name" keys change, taking on their values from the right hand side.

However, it's important to note that if we try to add a new key to this map, it will not work with this syntax:

iex> izzy = %{"name" => "Izzy", "age" => "30ish", "gender" => "Female"}
%{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}
iex> %{izzy | "country" => "Australia"}

** (KeyError) key "country" not found in: %{"age" => "30ish", "gender" => "Female", "name" => "Izzy"}
    (stdlib) :maps.update("country", "Australia", %{"age" => "30ish", "gender" => "Female", "name" => "Izzy"})

This pipe merging feature will only work with existing keys in the map, and this is an important difference to notice from Map.merge/2, which will work with both kinds of keys: new and pre-existing.

delete

We've now looked at how to add new keys or update keys in a map. The only thing left to look at is how to delete them. We can delete keys in a map by using the Map.delete/2 function. Let's say we didn't really care what country Izzy came from. We can delete that data, but keep everything else, by using Map.delete/2

izzy = Map.delete(izzy, "country")
%{"age" => "40ish", "gender" => "Female", "name" => "Izzy"}

This function takes a map as its first argument, and then a key as its second argument. When the function acts, it removes that key from the map.

Summary

In this chapter, we've seen a couple of ways of working with maps. We saw that we could go through each key-value pair in the map with Enum.each/2. We also used Enum.map/2 and Enum.into/2 to change the values inside the maps to other values, converting temperature numbers to fahrenheit ones.

When we looked at Enum.map/2 and Enum.into/2, we discovered that we could write the code in a shorter way with the pipe operator: |>. This operator allows us to pass the result of one function call to the next, allowing us to write cleaner Elixir code.

We rounded out the chapter by looking at some common functions from the Map module.

We saw Map.get/2 which allowed us to pick out a particular value based on a key's name. We knew we could already do this with code like person["name"], but, as we saw, Map.get/2 is useful if we're in the middle of a piping operation and need to pick out a value from a map.

We then looked at Map.put/3 which gave us the ability to create a new map with new keys from an existing map. This function took a map, a key and a value and then gave us back a new map with the changes we asked for.

If we were applying multiple changes with Map.put/3 at the same time, we learned that it wasn't smart to do that and so we then learned about Map.merge/2. We saw that this function takes two maps and merges them together, with the 2nd maps keys taking precedence over the first map's keys.

The last thing we saw in this chapter was how to get back a map without certain keys with Map.delete/2. It's a good way of removing data from a map if we don't need it anymore.

We're now moving out of the Enum, List and Map woods into something brand new to us: using Elixir to interact with files on our computer.

Exercises

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

11. Working with files

So far, we've worked with data that we've defined ourselves in the iex prompt. This has been incredibly handy because it has allowed us (and Izzy) to experiment with Elixir's features. When we program in the real world, data typically comes from sources external to our code. One of those sources is files from the file system.

In this chapter, we'll look at how we can read existing files, create new ones and even delete files using functions that are built-in to Elixir.

Reading a file

The first thing we're going to take a look at here is how to read a file's contents and then use Elixir to do something with those contents.

Certain files may contain data that we can use in our Elixir programs. Elixir can read any file you throw at it. In this chapter, just for simplicity, we're going to stick to a single file format: text files.

A text file is just a file with a bunch of words in it. You probably have a file like that on your computer right now. You could open one of these files in your text editor and read what was written in that file. If for some weird reason you don't have one of these, you can take this content and put it in a new file called haiku.txt:

rixilE evol I
nrael ot ysae os si tI
edoc lanoitcnuf taerG

I've put this file in a directory on my computer called "Joy of Elixir":

Figure 11.1: The haiku.txt file on my Mac within the Finder
And here's the content of that file that I see when I double-click on it:
Figure 11.2: The haiku.txt file's contents within my Mac's TextEdit program

Izzy squints at the haiku. "Um... This haiku looks a little... backwards". Yes, Izzy! Each line is written backwards! The haiku should read:

I love Elixir
It is so easy to learn
Great functional code

We could go in to the file and correct this ourselves, but we're programmers learning a super-duper-awesome programming language and by golly if we aren't going to use it to solve every problem we come across. To solve this problem, we're going to use the power of Elixir.

What we're going to do is to read this haiku into Elixir, and then we're going to reverse the order of words in each line. I make it sound so easy, and that's because it is. Really!

To read this file, we'll need to open an iex prompt where the file is, and then we can use this code to read it:

iex> File.read("haiku.txt")

This code tells Elixir to read a file by calling the File.read/1 function. Calling this function will give us the following output:

{:ok, "I evol rixilE\nnrael ot ysae os si tI\nedoc lanoitcnuf taerG\n"}

In this output, there's not one but two new concepts. You've gotten so far through the book that you're now so great at learning and that means I can introduce things rapid-fire and you'll pick 'em up with no effort.

The first of these is the curly brackets. Did you notice them? They wrap all of the output from the File.read/1. Did you notice that these curly brackets, unlike all the other curly brackets we've seen before, are not prefixed with a percent-sign (%)?

This particular concept in Elixir is called a tuple. You can think of tuples as a fixed-length list and they're used to link a bunch of information together in a particular order. In this case, it's telling us that the file operation was :ok, and then it's giving us a string containing all the file's information.

Izzy pipes up: "What's that :ok thing and why does it have a colon before it?". Good spot, Izzy. That is called an atom. We first saw these back in Chapter 4 when they were used as keys in maps.

We've seen atoms since then, but here's a refresher: Atoms are used to provide informational messages, like in this case. Atoms names are their values. This is unlike strings, maps and lists where we would assign to a variable in order to give a meaningful name to. This atom is telling us that the operation we asked File.read/1 to perform went "ok"; we were able to read the file successfully.

If we specified a file that wasn't present, File.read/1 would give us a different atom in that first place:

iex> File.read("haiboo.txt")
{:error, :enoent}

This cryptic error message uses a tuple containing two atoms, :error and :enoent. The first one is self-explanatory -- there was an error loading this file. The second one gives us a non-regular-human-friendly error message: :enoent. This is computer-lingo for "I couldn't find that file you were looking for, sorry."

The best part about these tuples and atoms being returned from the File.read/1 call is that we can use pattern matching (Chapter 6). If we want to only proceed if our File.read/1 function executes successfully, we can pattern match like this:

iex> {:ok, contents} = File.read("haiku.txt")

Go ahead and try this code out in your iex prompt. Also try it with the wrong path too and see what happens. If you give it the wrong path, you should see it fail like this:

** (MatchError) no match of right hand side value: {:error, :enoent}

This error happens because we're telling Elixir to expect that the tuple returned from this call contains an :ok atom at the start, but it doesn't. Pattern matching can be a useful way of stopping your program in its tracks like this.

Let's look back at the successful read:

iex> {:ok, contents} = File.read("haiku.txt")

You'll see exactly the same values come back as when we did the first File.read/1 invocation:

{:ok, "I evol rixilE\nnrael ot ysae os si tI\nedoc lanoitcnuf taerG\n"}

The difference is: this time, we've got the contents of the file in a contents variable. Oooh, that was sneaky! Let's get back to the task at hand: we still need to correct this haiku back to its proper form. We've now got the contents of this file and we need to reverse each line. To do that, we need some way of splitting apart the string so that we can process each line separately from each other line. To do this, we can use our old friend, String.split/3:

iex> contents |> String.split("\n", trim: true)

This code takes the string stored in contents and passes it as the first argument to String.split/3. The other two arguments are: 2) the string "\n" and 3) the option trim: true. The 2nd argument tells String.split/3 to split the string on the newline characters (\n), and the 3rd argument tells String.split/3 to remove any trailing space at the end.

When this function does its thing, we'll see this output:

["rixilE evol I", "nrael ot ysae os si tI", "edoc lanoitcnuf taerG"]

Yay! We've now got a list of strings here. We need each string here to be reversed. Izzy sticks her hand up and wiggles it around. "Ooh ooh ooh I know how to do this! String.reverse/1!", she says, monospaced font and all. Impressive. Yes, Izzy is correct! We can reverse a string by calling String.reverse/1 as we first saw back in Chapter 8. We know we can do it with a single string like this:

iex> "rixilE evol I" |> String.reverse
"I love Elixir

But we don't have a single string here, we have three strings within a list. But we have knowledge on our side. We have special skills that we built up in Chapter 9, and with those special skills we know that we can call Enum.map/2 to apply a function to multiple elements within a list. We've done exactly this back in Chapter 9:

iex> Enum.map(cities, &String.capitalize/1)

So let's take our list, adapt this code a little bit to use the pipe operator and String.reverse/1 and reverse each string with this code:

iex> contents \
|> String.split("\n", trim: true) \
|> Enum.map(&String.reverse/1)

We're using a multi-line pipe operator chain here to accomplish what we want. Because we're in iex, we need to put a backslash (\) on the end of every line to tell Elixir to treat all these lines as one long line and one chain of operations. If we were writing code in an Elixir file, we wouldn't need to do this. iex is a little special in this regard. Another way we could've written the code is like this:

iex> contents |> String.split("\n", trim: true) |> Enum.map(&String.reverse/1)

But it is considered best practice to split long pipe chains up onto multiple lines when they're really long just to help with readability. Think of it as the same rule that applies when you're writing a book: you split logical breaks into separate paragraphs to make it easier for people to read here. We're applying almost that same rule to our Elixir code.

This code (in either the one-line or three-line form) will give us back a list of strings that now look like proper English:

["I love Elixir", "It is so easy to learn", "Great functional code"]

All we need to do now is to put the file back into a big string, which we can do with a new-to-us function called Enum.join/2:

iex> fixed_contents = contents \
|> String.split("\n", trim: true) \
|> Enum.map(&String.reverse/1) \
|> Enum.join("\n")
"I love Elixir\nIt is so easy to learn\nGreat functional code"

Excellent! We've now got our file's text around the right way. We did this with a combination of quite a few functions and that is really demonstrating the repertoire of things that we know how to do with Elixir now. We have used File.read/1 function to read this file's contents and to bring them into our Elixir code. Once we have the contents there, we can do whatever we wish with them.

Before we move on, I want to show you another way that Elixir has for reading files.

Streaming a file

Elixir provides us at least one very clear way to read a file: File.read/1. We know how this works. But there is another function that Elixir gives us which can also be used to read files. That function is called File.stream!/1.

This function works by reading a file one line at a time. This is useful in cases where we might be reading very large files. If we used File.read/1 to load a large file, it might take a long time for Elixir to read it all in and it might take up a lot of our computer's resources. On the contrary, File.stream! can be used to read an entire file, but will read a file line-by-line.

We call this type of thing in programming parlance "eagerness" or "laziness". The File.read/1 function can be said to be eager: it eagerly loads the whole file without a care in the world if we're going to use the whole lot or not. While the File.stream!/1 function is its more chilled out cousin: it will lazily read a single line at a time from the file.

Think of it in terms of your favourite internet streaming service. When you stream from that service, you don't have to wait for every single episode of your favourite Scandi Noir TV series to download. You don't even have to wait for a single episode to fully download because it only sends through a small part of the episode at a time. This is what File.stream/1 does: gives us the file line-by-line.

Ok, I think we've talked enough about what File.stream/1 does! It's time to see this in action. Let's use this code to start reading our haiku.txt file again:

iex> stream = File.stream!("haiku.txt")
%File.Stream{
  line_or_bytes: :line,
  modes: [:raw, :read_ahead, :binary],
  path: "haiku.txt",
  raw: true
}

This time, we're not given back the contents of the file. We're given back something we've never seen before: a struct.

We can think of structs as maps that follow a particular set of rules. Just like maps, structs have key-value pairs, as we can see from the output here. If we removed the File.Stream part of this output, the syntax would be a map:

iex> %{
  line_or_bytes: :line,
  modes: [:raw, :read_ahead, :binary],
  path: "haiku.txt",
  raw: true
}

What makes a struct different here is that it is named and structs with the same name always have the same keys. Every time we call File.stream!, we'll see a File.Stream struct come back with exactly the same keys.

The keys in this particular struct tell Elixir what this file stream is all about. It tells Elixir that we're going to read from the haiku.txt (path) file line-by-line (line_or_bytes). I won't go into what modes and raw are for here.

At this point, all we have this File.Stream struct. Consider it as an "intent to read" the file. No reading has occurred at this point yet. To trigger this reading, we can pipe this stream into any Enum method. For instance, we could trim all the newline characters (\n) off each line like this:

fixed_contents = stream \
|> Enum.map(&String.trim/1)
["rixilE evol I", "nrael ot ysae os si tI", "edoc lanoitcnuf taerG"]
  

That's a good start. We're not going to be able to peer into exactly what Elixir is doing here, so you'll have to take my word for it that Elixir is now reading each line in this file one at a time and sending it through to Enum.map/2. The output from calling Enum.map/2 is good, but what we really need is a list of sentences the right way around. We can flip these by piping the output again:

fixed_contents = stream \
|> Enum.map(&String.trim/1) \
|> Enum.map(&String.reverse/1)
["I love Elixir", "It is so easy to learn", "Great functional code"]

That's getting better! Now we need to join these back together again and put line breaks in between them with Enum.join/2 again:

fixed_contents = stream \
|> Enum.map(&String.trim/1) \
|> Enum.map(&String.reverse/1) \
|> Enum.join("\n")
"I love Elixir\nIt is so easy to learn\nGreat functional code"

Excellent! We now have the fixed haiku in string form inside Elixir. We could get this through either File.read/1 or File.stream!/1. Normally, we would only use File.stream/1 if the file was really long, but in this section we've used it for illustrative purposes and to prove that there's more than one way to read a file in Elixir.

It would be really handy if we could take this string out of Elixir and put it into a new file so that nobody else has to read a backwards haiku. That's what we'll be looking at next!

Creating and writing to a new file

We now have the re-arranged contents of our haiku stored within a variable called fixed_contents. If you've lost this since the last section, you can use this code to get it back:

iex> fixed_contents = "haiku.txt" \
|> File.stream! \
|> Enum.map(&String.trim/1) \
|> Enum.map(&String.reverse/1) \
|> Enum.join("\n")

The pipe operator makes this code so much easier to read! Now that we most certain have the file contents stored in Elixir and put the right away around, we want to write these contents to a new file. We saw that we could read a file with File.read/1, so it would make sense for there to be a File.write function too. It definitely does exist, and it's called File.write/2. It takes two arguments: 1) the path we want to write to 2) the contents that we want to put in the file. It works like this:

iex> File.write("fixed-haiku.txt", fixed_contents)
:ok

Elixir dutifully takes the file path and the fixed contents and puts them into a file called fixed-haiku.txt. Well, we assume so. All that we know about this operation is that Elixir thought it went OK, as indicated by the atom that it returned: :ok.

If we want to make sure that the fixed-haiku.txt file is definitely there and contains what we expect it to contain, we could simply find that file in the file browser on our computers and double click on it. That would be the easiest way. But, because we're programmers and we've got powerful new skills up our sleeves, we can use what we know to check this. Let's use File.read/1 to see if that file is there:

iex> File.read("fixed-haiku.txt")
{:ok, "I love Elixir\nIt is so easy to learn\nGreat functional code"}

Great! Our file has definitely been written with the right content. Our haiku is in the right order.

Renaming a file

Izzy asks another question: "Why don't we switch these haiku.txt and fixed-haiku.txt files around? If I was opening a file called haiku.txt, I would expect that to be a proper Haiku, not this weird reversed one! I wouldn't think to look in fixed-haiku.txt.

Izzy's right. It's weird that haiku.txt doesn't contain the haiku. We should rename this strange reversed version to reversed-haiku.txt, and then rename fixed-haiku.txt to haiku.txt to ease any confusion people might have.

To read a file there was File.read/1. To write a file there was File.write/2. So it makes sense that in order to rename a file, there is File.rename/2. Elixir is pretty sensible like that, if you haven't realised already. File.rename/2's two arguments are the original name of the file, and then the new name we want.

Let's use this function to first rename haiku.txt to reversed-haiku.txt:

iex> File.rename("haiku.txt", "reversed-haiku.txt")
:ok

Elixir has told us that this operation has succeeded. Well, if that's the case then we should see when we look into that directory again that the haiku.txt file isn't there, but a reversed-haiku.txt file is there instead. Oh, and fixed-haiku.txt exists too. Let's take a peek:

Figure 11.3: Two files now exist, fixed-haiku.txt and reversed-haiku.txt.

Yes! Our file has been renamed successfully. That's a good start. Let's rename fixed-haiku.txt into haiku.txt with File.rename/2 too:

iex> File.rename("fixed-haiku.txt", "haiku.txt")
:ok

Elixir tells us that the file rename operation was successful yet again. Two for two! Peeking into that directory one more time, we'll see that indeed it was:

Figure 11.4: The haiku (haiku.txt), and its reversed version (reversed-haiku.txt) are safely in place.

Wonderful stuff. Whenever we need to rename a file within Elixir we now know that we can reach for File.rename/2.

So now we've seen how to read existing files, create new ones and rename them. The final file operation that we'll look at this chapter is to delete the files.

Deleting a file

We've all deleted files from our computers in our lifetime. Sometimes even on purpose. The files reach a point where they're no longer useful and we want them gone. The way we might do this is to drag the file to the Recycle Bin, or to right click and choose "Delete", or... well, there's so many ways to delete files.

Elixir provides us a way to delete files too, but it isn't as intuitively named as the operations for reading (File.read/1), writing (File.write/2), and renaming (File.rename/2). To delete a file in Elixir we call the function called File.rm/1.

"RM? Like R.M. Williams, the world famous Australian manufacturer of leather things? What does leather shoes and belts have to do with removing files?", Izzy asks with quite the perplexed look on her face, cork hat bobbling quizzically along. Woah, slow down there Izzy. Elixir's File.rm/1 has nothing to do with boots!

This File.rm/1 file is called rm as a shorthand for "remove". We can use this function to remove any file we like. We would like to keep our haiku files, so let's create a separate file that we can remove later by using File.write/2 first, and then we'll delete it with File.rm/1. Let's do this slow so that we can see that the file definitely does exist before we delete it. Let's start with creating the file:

iex> File.write("delete-me.txt", "delete me")
:ok

If we look in our directory, we can see that this file definitely exists:

Figure 11.5: The delete-me.txt file exists.

Let's try removing this file now:

iex> File.rm("delete-me.txt")
:ok

Elixir says that removing this file was successful. Let's take a look at the directory:

Figure 11.6: The delete-me.txt file is gone!

The file is gone!

We've now got our head around some of the essential things that we can do with files with Elixir. We know that we can read existing files with File.read/1, write completely new ones with File.write/2 and delete existing files with File.rm/1. For good measure, we also learned that these functions may return tuples that indicate errors, like {:error, :enoent}, which tells us that files do not exist.

We've now encountered a situation (well, situations) in Elixir where we can call code and get different outcomes depending on external factors like whether files exist or not. The code that we've written so far hasn't been particularly resilient to this sort of thing. We've become accustomed to running code and always seeing the same result.

But the rules have changed. We now will need to account for this sort of thing. In the chapter I will show you a few ways that we can make our code more resilient by making it behave differently, depending on the circumstances.

Exercises

  • Can you make Elixir write a program for itself? Put this code into a file called script.ex with File.write/2: IO.puts "This file was generated from Elixir" and then make it run by running elixir that-file.ex.
  • 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?

12. Conditional code

In the last chapter, we have encountered a situation where our code can have different outcomes. Outcomes that we cannot predict from within the code itself. As an example, when we call File.read/1 we could have two main outcomes:

  1. The file exists, and so the file is read successfully. We get back an {:ok, contents} tuple.
  2. The file does not exist, and so we get back a {:error, :enoent} tuple instead.

We have dealt with this in the past by making our program crash if we don't get an expected value. We do this by pattern matching on the outcome that we expect.

iex> {:ok, contents} = File.read("haiku.txt")

In this example, if File.read/1 succeeds then everyone is happy. We're expecting to get back a tuple here with two elements: 1) an :ok atom, and 2) the contents of the file.

But if our expectations aren't met because the haiku.txt file is missing, then things aren't so great:

{:ok, contents} = File.read("haiku.txt")
** (MatchError) no match of right hand side value: {:error, :enoent}

Elixir tells us that something went wrong and will refuse to do anything more about it until we give it different instructions.

We need to cater for this sort of thing happening in the Elixir programs that we write. Sometimes, files are missing. The way that we can handle this is by using some conditional code within Elixir. Elixir has four main helpers for conditional code. They are: case, cond,if, and with. Conditional code allows us to give Elixir different instructions depending on what happens during the running of any Elixir program. Let's look at some examples.

case

As we just talked about, we saw a case where file reading could fail. When we call this function, it can have two outcomes:

  1. The file exists, and so the file is read successfully. We get back an {:ok, contents} tuple.
  2. The file does not exist, and so we get back a {:error, :enoent} tuple instead.

In both cases, we get back a tuple, but what we can do with that tuple is dependent on what's inside. If we get back {:ok, contents} then we can carry on working with that file. But if we get {:error, :enoent} then we will have to make our program stop.

One of the ways to get Elixir to behave this way is to use case:

iex> case File.read("haiku.txt") do
  {:ok, contents} ->
    contents
    |> String.split("\n", trim: true)
    |> Enum.map(&String.reverse/1)
    |> Enum.join("\n")
  {:error, :enoent} ->
    IO.puts "Could not find haiku.txt"
end

This case statement uses much of the same code we saw in the previous chapter, but now goes different routes depending on the output of File.read/1. If File.read/1 returns something that matches the pattern of {:ok, contents}, then our file's code will be parsed and reversed correctly, resulting in this output:

"I love Elixir\nIt is so easy to learn\nGreat functional code"

However, if that File.read/1 call results in something that matches {:error, :enoent}, then we will see an error message telling us that we couldn't find that file.

Could not find haiku.txt

These two "forks in the road" for this case statement are referred to as clauses.

You might recognise this code as being similar to a function we defined back in Chapter 6:

iex> road = fn
  "high" -> "You take the high road!"
  "low" -> "I'll take the low road! (and I'll get there before you)"
  _ -> "Take the 'high' road or the 'low' road, thanks!"
end

This is because it is the same underlying principle. We're using pattern matching inside the case in this chapter to determine what to do, just like we did in that function 6 chapters ago. Before the -> we tell Elixir what we want to match. Then after that, we tell it what we want to do once that match happens. In our case statement, we put that "after" code on separate lines and this is just to increase readability of the code. We could've done the same in our function too:

iex> road = fn
  "high" ->
    "You take the high road!"
  "low" ->
    "I'll take the low road! (and I'll get there before you)"
  _ ->
    "Take the 'high' road or the 'low' road, thanks!"
end

You might notice that in this function block, we have the catch-all clause at the end (_ ->). This is the last-ditch effort for the function to do something. It's worth knowing that we could do the same thing in our case statements too:

iex> case File.read("haiku.txt") do
  {:ok, contents} ->
    contents
    |> String.split("\n", trim: true)
    |> Enum.map(&String.reverse/1)
    |> Enum.join("\n")
  {:error, :enoent} ->
    IO.puts "Could not find haiku.txt"
  _ ->
    IO.puts "Something unexpected happened, please try again."
end

In this code, if Elixir sees something that is not known to the case statement then it will give us a completely different message. While we're on this topic of catch-all clauses, I want to show you one more precise way of doing this too:

iex> case File.read("haiku.txt") do
  {:ok, contents} ->
    contents
    |> String.split("\n", trim: true)
    |> Enum.map(&String.reverse/1)
    |> Enum.join("\n")
  {:error, :enoent} ->
    IO.puts "Could not find haiku.txt"
  {:error, _} ->
    IO.puts "Something unexpected happened, please try again."
end

This time, our last clause will not match everything and anything. It will only match tuples that have exactly two items in them, and the first item must be :error. As we can see here, this is using the pattern matching feature in Elixir that we've seen a few times throughout this book already. The tuple for the last clause isn't exactly {:error, _}, but it is something that is in the same pattern. This pattern matching is why the last clause would match any other error that may be returned from File.read/1.

This is a better approach, because it is clearer to anyone else seeing this code what we might expect when something unexpected happens.

Now we know Elixir has two places where these clauses are used: functions and case blocks. We're about to see another one.

cond

The case statement is good to use if you want to compare the outcome of one particular action and do different things depending on whatever that outcome is. In the last section, that outcome was the output of a File.read/1 function call.

What we'll see in this section is that case has a cousin called cond which provides us a way of checking multiple conditions (cond is short for "condition"), and then running some code for whatever clause is true. Here's a quick example:

iex> num = 50
50
iex> cond do
  num < 50 -> IO.puts "Number is less than 50"
  num > 50 -> IO.puts "Number is greater than 50"
  num == 50 -> IO.puts "Number is exactly 50"
end
Number is exactly 50
:ok

Izzy asks: "What does <, > and == mean? We've never seen those before!" Yes! This is the first time in twelve chapters that we've seen these things. Now is a great time to cover what they do. <, > and == are ways to compare two values in Elixir. You can probably guess from the code that < means "less than", > means "greater than", and that == is "exactly equal to". But what is this code actually doing?

If we take the comparisons out of the cond and run them individually, we'll have a clearer picture of what this code is doing:

iex> num > 50
false
iex> num < 50
false
iex> num == 50
true

These comparisons are going to compare the two values and then tell us if those comparisons are true or false. This is our first exposure to code that outputs either true or false. Think of it like this: if we were to ask the question of "is num equal to 50", what would the answer be? We would normally say "yes, it is equal". Elixir's version of an answer to this question is true.

When we use these comparisons in cond, the first clause where the comparison results in true will execute that clause's code. Go ahead and change the number in the cond code above to see how it might react to those changes.

if, else and unless

Now that we've seen what case and cond can do, let's look at two more related conditional statements: if and unless and their compatriot else.

The cond statement was really helpful if we had multiple conditions to compare against. In the previous code, we wanted to check if the number was less than, greater than or exactly equal to 50:

iex> num = 50
50
iex> cond do
       num < 50 -> IO.puts "Number is less than 50"
       num > 50 -> IO.puts "Number is greater than 50"
       num == 50 -> IO.puts "Number is exactly 50"
     end
Number is exactly 50
:ok

But what if we only wanted to check if the number was exactly 50? Well, we could remove the first two clauses from this cond statement:

iex> cond do
       num == 50 -> IO.puts "Number is exactly 50"
     end
Number is exactly 50
:ok

This is one way of writing this code and it will work perfectly fine. However, if the number was not 50, then we would see an error come from this code:

iex> num = 10
10
iex> cond do
       num == 50 -> IO.puts "Number is exactly 50"
     end
** (CondClauseError) no cond clause evaluated to a true value

This is happening because cond always requires at least one of its conditions to evaluate to a true value. In the code we've just attempted, num == 50 will be false, not true, and because it is the only clause in this cond we will see this error.

If we've got code like this in Elixir where we're running code conditionally and we don't want Elixir to show us big scary error messages like this one, we should be using if instead. Let's look at how we could attempt the same code with if:

iex> num = 10
10
iex> if num == 50 do
        IO.puts "Number is exactly 50"
      end
nil

Because the condition here is not true, nothing happens. The way that Elixir represents nothing is with nil. We asked Elixir to only execute code if the condition is true, but it wasn't. So nil is the outcome.

unless

Now we've seen how to do something if a particular condition is true, but what happens if we want to do something if it is false? For this, Elixir gives us unless:

iex> num = 10
10
iex> unless num == 50 do
        IO.puts "Number is not 50"
      end
Number is not 50
:ok

In this short example, Elixir will output our "Number is not 50" message if the number is not 50. If the number is 50, then nothing (nil) will be the result of this code.

If you're unsure of whether to use if or unless, try reading the code out loud. Does it make more sense to say "unless the number is equal to 50"? In this case, yes it does. But let's try another example:

iex> num = 10
10
iex> unless num != 50 do
        IO.puts "Number is 50"
      end
Number is 50
:ok

This time, the code reads in English as "unless the number is not equal (!=) to 50". This sentence contains a double negative with the use of "unless" and "not", and so using unless in this example is unsuitable. The code should use an if instead:

iex> num = 50
50
iex> if num == 50 do
       IO.puts "Number is 50"
     end
Number is 50
:ok

Now the code reads as "if the number is equal to 50", and that makes a lot more sense!

else

We've now seen if and its opposite unless, but what if we wanted to do if and unless at the same time? What if we wanted Elixir to do some code if a particular condition was true, but then do some other code if it wasn't true?

For this, Elixir gives us else:

iex> num = 10
10
iex> if num == 50 do
       IO.puts "Number is 50"
     else
       IO.puts "Number is not 50"
     end
Number is not 50
:ok

This would read like: "if the number is 50, show 'Number is 50', otherwise, show 'Number is not 50'". In this code, our number is not 50, and so we see Elixir tell us that.

with

There's one more feature of Elixir to do with conditional code that I would love to show you before we finish off this chapter. It is called with. The with feature allows us to chain together multiple operations, and only continue if each of those operations is successful. Before we look at how to use with, let's look at the type of problem it is good at solving.

Let's say that we had somehow came across a map containing this data in our travels:

iex> file_data = %{name: "haiku.txt"}

This map is designed to tell us what file to read our haiku from. This map contains a single key which is the atom :name, and its value is the string "haiku.txt". So we could know by accessing the name key in this map what file to read from. Here's one way we could do it:

iex> File.read(file_data["name"])
{:ok, "rixilE evol I..."}

But what would happen here if the map didn't contain this key, but instead a differently named key? Then our code would break:

iex> file_data = %{filename: "haiku.txt"}
%{filename: "haiku.txt"}
iex> File.read(file_data["name"])
{:error, :enoent}

Our programs need to be written in such a way to protect against this sort of thing. In this particular case, we must make them expect to read a value from a key called :name, not :filename. Once it has done that, then it can try to read that file.

One way to write this would be to use a case statement inside another case statement, like this:

file_data = %{name: "haiku.txt"}
case Map.fetch(file_data, :name) do
  {:ok, name} ->
    case File.read(name) do
      {:ok, contents} ->
        contents
        |> String.split("\n", trim: true)
        |> Enum.map(&String.reverse/1)
        |> Enum.join("\n")
      {:error, :enoent} ->
        IO.puts "Could not find a file called #{name}"
    end
  :error -> "No key called :name in file_data map"
end

In this code, we attempt to find the :name key in file_data with Map.fetch/2. This Map.fetch/2 function is new to us here, and so we'll quickly cover what it does.

The way Map.fetch/2 works is that if there is a key in the map then Map.fetch/2 will return {:ok, name}. If there isn't, it will return the atom :error.

We pattern match on either of these two outcomes during this case statement. In this example, there is going to be a :name key, and so it will match the {:ok, name} part of our case statement. Then the second case statement will come into effect.

This second case statement attempts to read a file using File.read/1. If this file exists, then this function will return {:ok, contents}. If the file does not exist, {:error, :enoent} will be returned instead. In this case, we know that the file exists from our previous attempts at reading it, and so this code will execute successfully.

This code is a little hard to read, as we need to really focus to keep our mind on where we are in the code. A with statement can simplify this code:

with {:ok, name} <- Map.fetch(file_data, :name),
     {:ok, contents} <- File.read(name) do
  contents
  |> String.split("\n", trim: true)
  |> Enum.map(&String.reverse/1)
  |> Enum.join("\n")
  |> IO.puts
else
  :error -> ":name key missing in file_data"
  {:error, :enoent} -> "Couldn't read file"
end

In Elixir, with statements are to be read like a series of instructions of what to do when everything goes right. In this example, if Map.fetch/2 succeeds and returns {:ok, name}, then Elixir will be able to use it in the next step, when it calls File.read/1. After that, our code will work as intended.

However, if Map.fetch/2 fails, then :error will be returned. We handle that in the else inside this with block, telling with that if we see :error returned, that we want to see an error message saying that the :name key was missing. Then if File.read/1 fails and returns {:error, :enoent}, then we want it to tell us that it couldn't read the file.

This code is a little neater than the double-case code because all of our code that deals with the success of our program is grouped neatly at the top, and all the code that deals with the failure is grouped at the bottom.

I would encourage you to play around here with the with statement to get a better understanding of it. What happens if you change file_data so that it doesn't have a :name key? What happens if that haiku.txt file goes missing?

13. Finding more functions

We've now seen a small taste of a variety of functions that Elixir provides us. In this chapter, we're going to look at some new functions that will help us. These functions can show us documentation about other functions, tell us about variables within our code and more. We'll also see where we can go to find out more information

iex helper functions

The iex prompt lets us run Elixir code with ease and to also see the output of that code. We've seen plenty of examples of this throughout the last twelve chapters. But the iex prompt has at least two more tricks up its sleeves.

The h helper

The first of these tricks is the h helper. This helper's name is short for "help", and it will display documentation on whatever you wish. For instance, if you wanted to bring up the documentation for Map.get/2, you can do that by running this command inside of iex:

iex> h Map.get/2

When you run this, you'll see this output:

def get(map, key, default \\ nil)
Gets the value for a specific key in map. If key is present in map with value value, then value is returned. Otherwise, default is returned (which is nil unless specified otherwise). ## Examples
iex> Map.get(%{}, :a) nil iex> Map.get(%{a: 1}, :a) 1 iex> Map.get(%{a: 1}, :b) nil iex> Map.get(%{a: 1}, :b, 3) 3

This output shows us the documentation that Elixir has for the Map.get/2 function. It even shows us some examples of how to use this function, which is pretty great!

One thing we didn't learn back in Chapter 10 when we first learned about the Map.get/2 function is that it can take a third argument too, a default argument. This argument will be returned if the key we're trying to get isn't found, as we can see by the examples provided by the very helpful documentation.

"Hold on a minute!", Izzy pipes up. "If this function takes a third argument, then why is it called Map.get/2 and not Map.get/3?". Izzy makes a good point. The number at the end of Map.get/2 signifies how many arguments a function takes. We learned this back in Chapter 9. What we can see with this function is that there's a Map.get variant that takes three arguments, and so there is a Map.get/3 function, as well as the Map.get/2.

We can look up any function's documentation that we've seen so far. Go on, try looking up the documentation for functions that we've seen in the past chapters. They're all documented here!

The arrow and v helpers

Two more little helpers that you should know about are the arrows on your keyboard, and the v helper provided by iex. You might have been using the arrows already to move around in code. The up and down arrows allow us to move through the historical input in our prompt, while the v helper lets us pull values out of particular lines in the output.

Let's say that we've been working in iex and we've written this code:

iex(1)> %{name: "Izzy", age: "30ish", gender: "Female"}
iex(2)> %{name: "Ryan", age: "30ish", gender: "Male"}

Oh drat. We've just realised that wanted to assign these to variables, but forgot to. One way we can get that value again is by pressing the up-arrow on our keyboard. We'll then see this:

iex(1)> %{name: "Izzy", age: "30ish", gender: "Female"}
%{age: "30ish", gender: "Female", name: "Izzy"}
iex(2)> %{name: "Ryan", age: "30ish", gender: "Male"}
%{name: "Ryan", age: "30ish", gender: "Male"}
iex(3)> %{name: "Ryan", age: "30ish", gender: "Male"}

Our terminal helpfully brings back the last value when we push the up-arrow. But if we push the up-arrow again, look what happens:

iex(1)> %{name: "Izzy", age: "30ish", gender: "Female"}
%{age: "30ish", gender: "Female", name: "Izzy"}
iex(2)> %{name: "Ryan", age: "30ish", gender: "Male"}
%{name: "Ryan", age: "30ish", gender: "Male"}
iex(3)> %{age: "30ish", gender: "Female", name: "Izzy"}

We're now back to the first value again! We could then go to the start of this line (by pushing the left-arrow, or Ctrl+A) and assign it to a variable if we wished:

iex(3)> izzy = %{age: "30ish", gender: "Female", name: "Izzy"}
%{age: "30ish", gender: "Female", name: "Izzy"}

If we push the up-arrow again here, we'll get the same as line 3; the line we just had:

iex(4)> izzy = %{age: "30ish", gender: "Female", name: "Izzy"}

If we push it a second time, we'll get the same as line 2.

iex(4)> %{name: "Ryan", age: "30ish", gender: "Male"}

If we push the down-arrow, we'll go back to line 3.

iex(4)> izzy = %{age: "30ish", gender: "Female", name: "Izzy"}

This is how we can navigate up and down, or back and forward (depending your viewpoint) through the history of what we've put into iex.

Now about that v helper. If we want, we can pick a value out of a particular line by using v. For instance, if we wanted the value from the 2nd line of our iex prompt and then to assign that to a variable called ryan we can run this code:

iex(4)> ryan = v(2)
%{name: "Ryan", age: "30ish", gender: "Male"}

This code can be read as: find the 2nd value from the prompt, and assign it to a variable called ryan. As this v helper is a function in Elixir, We can even leave off the brackets if we wish to save us some typing:

iex(5)> ryan = v 2
%{name: "Ryan", age: "30ish", gender: "Male"}

Tab complete

One last iex helper that I want to show you involves another key on your keyboard: the Tab key.

In your iex prompt, you can hit the Tab key to make it autocomplete what you type. For instance, you can type this:

iex> String.rev

And then if you hit tab, iex will autocomplete it into String.reverse:

iex> String.reverse

You can do this with any module name or function you wish. Go on, give it a go with things like En[TAB], and Enum.fil[TAB].

You might realise if you experiment enough that if iex doesn't know what you want, then it will provide some suggestions. For instance, if we type Enum.f and hit tab, we'll see all the functions from the Enum module that start with f:

fetch!/2 fetch/2 filter/2
find/2 find/3 find_index/2
flat_map_reduce/3

We've seen the Enum.find/2 and Enum.filter/2 functions back in Chapter 9, but as we can see here Elixir provides a few more functions than the ones we've seen so far.

Keep on exploring here and seeing what other functions you can find. Remember that you can always use the h helper to bring up information about functions you don't know about yet. For instance, we could bring up the documentation for Enum.find_index/2 by writing this:

iex> h Enum.find_index/2

The documentation that we would see is this:

def find_index(enumerable, fun)
@spec find_index(t(), (element() -> any())) :: non_neg_integer() | nil Similar to find/3, but returns the index (zero-based) of the element instead of the element itself. ## Examples iex> Enum.find_index([2, 4, 6], fn x -> rem(x, 2) == 1 end) nil iex> Enum.find_index([2, 3, 4], fn x -> rem(x, 2) == 1 end) 1

There's also one more place where we can go to find documentation.

Elixir's documentation

As we've seen a few times now, we can bring up documentation in the iex prompt with the h helper. There's another place where we can go to find the same documentation, as well as documentation for every other function Elixir provides.

That place is https://elixir-lang.org/docs.html. When you visit that site, you'll see a list of documentation:

Figure 13.1: Elixir's Stable Documentation

The "stable" name at the top indicates that the documentation listed underneath is the for the most recent stable release of Elixir, where "stable" meaning that it's safe for people like us to use, and won't have any nasty bugs in it.

We can see a list of documentation here for the different parts of Elixir. There's "Elixir", which is the documentation for all the functions we've been using so far. You can see documentation for other parts of Elixir too, including "IEx", which gets its own set of documentation. There's also "Mix" which we'll find out about in Chapter 14, and "ExUnit" that we'll find out about in Chapter 15.

If we click that "Elixir" link near the top, we'll be taken to the documentation site for Elixir's standard library. A "standard library" is a set of functions that programming languages provides, and this new page will show you Elixir's.

The default page is a little wordy:

Figure 13.2: Elixir Standard Library Homepage

Let's not focus on all the words on the right-hand-side too much. What I want to show you here is the search bar over on the left.

Figure 13.3: Look to the left hand side and see the search box.

You can type any module or function name into this box to be taken right to its documentation. If we want to find out about Enum.find, we can search for that and then see this page:

Figure 13.4: Enum search results

If we click on the find/3 function, we'll be taken right to its location in the docs: https://hexdocs.pm/elixir/Enum.html#find/3. The great thing about this URL is that it's shareable, and so if you have friends that are learning Elixir too, you can give them this URL to teach them about Enum.find/3 too.

Here's what we'll see on this page when we visit it.

Figure 13.5: Enum find

The great thing here is that we'll be on a page that has all the documentation that Elixir has for the Enum module. We can scroll up (or down!) the page to find out what other functions Elixir provides in this module.

The other great thing about this is that the documentation here will match what we see when we use the h helper in the iex prompt:

def find(enumerable, default \\ nil, fun)
@spec find(t(), default(), (element() -> any())) :: element() | default() Returns the first item for which fun returns a truthy value. If no such item is found, returns default. ## Examples iex> Enum.find([2, 4, 6], fn x -> rem(x, 2) == 1 end) nil iex> Enum.find([2, 4, 6], 0, fn x -> rem(x, 2) == 1 end) 0 iex> Enum.find([2, 3, 4], fn x -> rem(x, 2) == 1 end) 3

So now we have two ways of looking at documentation for the functions that we're using within Elixir.

Elixir School

It would be remiss of me to talk about documentation and not to mention the other excellent source of documentation, written by the Elixir community for the Elixir community: Elixir School.

This site covers a lot of the same topics that we've covered -- and will cover in this book -- but in some particular cases it can provide different explanations for things. Now that you're this far into your Elixir journey, you should be able to read this and glean further knowledge from this site to bolster all the knowledge you've gained thus far.

Summary

In this chapter, we've looked at how we can be more productive by reading documentation and learning from that, as well as new tricks for moving around in the iex prompt.

We saw the h helper, which brings up documentation about a particular module or function. We also saw that we can press the up and down arrows on our keyboards to navigate through the history of what we've typed into iex

Lastly, we saw that Elixir has online documentation that we can read too, and we saw that this documentation matches exactly what we can see in our iex prompt.

While we were looking at that documentation, we saw mentions of things called "Mix" and "ExUnit". These are two additional parts of Elixir's tooling that we will be looking at in the 4th and final part of this book: Chapters 14 and 15.

14. Modules and Structs

We've now seen lots of examples of functions throughout this book. We've seen how we can define anonymous functions (in Chapter 5):

iex> hello = fn (place) -> "Hello #{place}!" end
hello.("World")
"Hello World!"

And we've seen how we can call functions from already defined modules (in Chapter 8):

iex> String.downcase("HELLO BUT QUIETLY")
"hello but quietly"

But what we haven't seen yet is how to define our own modules, or even why we would want to do that. The why is the simple part: we define functions inside of modules to keep them separate from other functions; modules are a convenient way of grouping functions. Here's a refresher that recycles the description used in Chapter 8:

The functions that Elixir provides are separated into something akin to kitchen drawers or toolboxes, called modules. Whereas in your top kitchen drawer you might have forks, knives, sporks, and spoons (like every sensible person's kitchen does), and in another you might have measuring cups, and in another tea towels.

In Elixir, the functions to work with the different kinds of data are separated into different modules. This makes finding functions to work with particular kinds of data in Elixir very easy.

We haven't yet built a complex enough system to require us to put functions inside of modules, or to even want to write our own functions. That changes in this chapter! A rumbling occurs behind us, and slightly to our left. It's Izzy vibrating with anticipation. It's a weird and unearthly sound, but we'll roll with it.

This chapter will show you how to write new modules for the purpose of grouping together functions, and we'll also cover a special kind of map called a struct. Let's go!

Functions for the people

Way back in Chapter 4, we had maps that looked like this:

%{name: "Izzy", age: "30ish"}

In this chapter, we're going to be working with maps based off these ones, but they're going to be a little more complicated in shape:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}

With our new map shape, what if we wanted to have a function that joined together the person's first and last name into a single string? Well, we could probably write it like this:

iex> full_name = fn (person) -> "#{person.first_name} #{person.last_name}" end

Then we can call this function with this code:

iex> full_name.(person)
"Izzy Bell"

This is wilfully ignoring the fact that some people have mononyms, as well as the falsehoods programmers believe about names. Ignoring both of those things for now, we have a single function that will let us display a combination of a first name and a last name. That's all well and good to have a function that does that.

But what if we had another function that also operated on these people maps? A function called age:

iex> age = fn (person) ->
...> days =  Date.diff(Date.utc_today, person.birthday)
...> days / 365.25
...> end

This new function uses two new functions from Elixir's built-in Date module: Date.utc_today/0 and Date.diff/2. The Date.utc_today/0 function returns the current date in the UTC time zone. The Date.diff/2 function allows us to figure out the difference (in days) between two dates. So we're using this function to find the number of days between today and the person's birthday. We then take that number of days and divide it by the average number of days in a year: 365.25. This should give us a close-enough approximation of somebody's age.

We can call this function with this code:

iex> age.(person)
31.972621492128678

Note that you'll get a different number to the one shown here because Date.utc_today will return a different day for you. This number was correct at the time of writing, I promise!

Now we have two functions that work on this same map. Wouldn't it be nice to have a place where we could group the functions? Similar to how Date.diff/2 and Date.utc_today/0 functions. Those functions are grouped together inside of a module called Date. Let's look at how we can create our own module for these full_name and age functions.

The Person module

Elixir comes with a bunch of built-in modules: List, Map, Enum and Date are a few that we've seen so far. Creating modules is not just for Elixir itself, but we can create our own modules too. The main reason to create a module is to group together functions, which is exactly what we're going to do in this part of this chapter.

To define modules in Elixir we use defmodule. We could write it out in iex:

iex(1)> defmodule Person do
...> ...
...> end

But we're going to be changing this code a lot and so we should write the code in a file instead. Let's create a new file called person.exs inside a directory called ~/code/joy_of_elixir and we'll define the Person module in this file:

defmodule Person do
end

Our module doesn't do very much at the moment. So let's change that by putting our functions inside it. Functions defined in a module must start with the keyword def:

defmodule Person do
  def full_name(person) do
    "#{person.first_name} #{person.last_name}"
  end

  def age(person) do
    days = Date.diff(Date.utc_today, person.birthday)
    days / 365.25
  end
end

To use this module, we need to make sure that we're in the ~/code/joy_of_elixir directory and then we can run iex. Once we're in iex we can compile the module with:

iex> c "person.exs", "."

The c helper's name is short for "compile". Using c like this will load the code from person.exs into our iex session, making the Person module and its functions available to us.

When we run c, it outputs a list of the modules that were loaded by compiling the file, which we see here as [Person], since there's only the Person module in that file.

We're passing a second argument to c here which is a path where our compiled module should go. The dot (.) here means "the current directory", so where we ran the iex command from. When we run this command, Elixir will compile our module to a file called Elixir.Person.beam.

With this file compiled, we can start to use the Person module. Let's try the Person.age/1 function:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}
iex> person |> Person.age
31.975359342915812

Great! That one works. Let's try the full_name function too:

iex> person |> Person.full_name
"Izzy Bell"

Excellent. Both functions work!

One special thing to note here is that if we exit out of iex and re-open it again, our module will still be accessible. We will still be able to run the functions without first having to compile our module:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}
iex> person |> Person.age
31.975359342915812
iex> person |> Person.full_name
"Izzy Bell"

This is because Elixir will load the Elixir.Person.beam file automatically, as it is located in the directory that we're running iex in.

Now that we have got our module running, let's add another two functions to it. These functions will set a person's location to either be "home" or "away". This location value will indicate if the person is at home, or if they're away from home. The functions should go at the bottom of the module definition inside person.exs:

defmodule Person do
  def full_name(person) do
    "#{person.first_name} #{person.last_name}"
  end

  def age(person) do
    days = Date.diff(Date.utc_today, person.birthday)
    days / 365.25
  end

  def home(person) do
    %{person | location: "home"}
  end

  def away(person) do
    %{person | location: "away"}
  end

If we attempt to use these functions right away, they will not work:

iex> person |> Person.away
** (UndefinedFunctionError) function Person.away/1 is undefined or private

This is happening because we have not yet re-compiled the Person module. To do that, we need to use the c helper again:

c "person.exs", "."

With the module now compiled, we will be able to use this function:

iex> person |> Person.away
** (KeyError) key :location not found
person.exs:15: Person.away/1

Well, we thought we could. But this map doesn't have a location key on it, and this means the away function is unable to set that key to a value. We can fix this by providing that key when we define the initial map:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
  location: "home",
}
iex> person |> Person.away
%{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: "away"
}

Okay, so what we needed to do here was to provide the location key in the map and then it worked. That's good to see! But how could we prevent this missing key being an issue again? The way to do that is with a struct!

Structs

So far, we have been using maps to represent our people -- well, one person -- and then passing this map through to the functions from the Person module:

iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
  location: "home",
}
iex> person |> Person.away
%{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: "away"
}

We're now going to take the time to look at a data type that is similar to a map, called a struct. The word "struct" is shorted for "structured data"; it's a map, but a particular type of map, where each map of the same type will share the same structure.

A struct has a set of key-value pairs just like a map, but a struct's keys are limited to only a certain set. Let's look at how we would define a struct now within the Person module. At the top of the module, we can use the defstruct keyword to define a struct:

defmodule Person do
  defstruct [
    first_name: nil,
    last_name: nil,
    birthday: nil,
    location: "home"
  ]

  ...
end

Let's start iex and compile our person.exs file:

iex> c "person.exs"

To use this struct, we use a similar syntax to how we would create a new map:

person = %Person{}

The difference is here that we put the name of the module where the struct is defined between the % and the opening curly bracket. In our defstruct call, we have defined a default value for location: "home". So when we've now built this new struct with %Person{}, it will already have a :location key set to "home".

This is one of the advantages of structs over maps: structs can have default values. This will avoid the issue where our away function was failing because there was no location key in our map.

One extra thing that will help here is to match on the struct type in the away and home functions. This will ensure that we always are getting a Person struct before we try to do anything in these functions. To do this, we need to change these functions to this:

def home(%Person{} = person) do
  %{person | location: "home"}
end

def away(%Person{} = person) do
  %{person | location: "away"}
end

This change to these functions will make them always require a Person struct as an argument. They will no longer work with a map. Let's see this in action by re-compiling our module and trying agani:

iex> c "person.exs", "."
iex> person = %{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
  location: "home",
}
iex> person |> Person.away

When we run this code, we'll see this error:

** (FunctionClauseError) no function clause matching in Person.away/1

The following arguments were given to Person.away/1:

    # 1
    %{
      birthday: ~D[1987-12-04],
      first_name: "Izzy",
      last_name: "Bell",
      location: "home"
    }

Attempted function clauses (showing 1 out of 1):

    def away(%Person{} = person)

person.exs:22: Person.away/1

This error is showing us that the function does not match. It shows us that we're passing a plain map to the function as its first argument, but the function is expecting a Person struct instead. So let's pass one of those instead:

person = %Person{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}
iex> person |> Person.away
%Person{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: "away"
}

That works a lot better! And did you notice that we didn't have to supply a location for our Izzy either? The struct will use the default value if we do not specify it.

By enforcing a Person struct here in this away function, we can be guaranteed that the function will always receive a person argument that is a Person struct, and that means it will always have a location key.

While we're here, we should also make the same changes to the age and full_name functions too, just to make sure that we receive structs for those functions too.

Our module will now look like this:

defmodule Person do
  defstruct [
    first_name: nil,
    last_name: nil,
    birthday: nil,
    location: "home"
  ]

  def full_name(%Person{} = person) do
    "#{person.first_name} #{person.last_name}"
  end

  def age(%Person{} = person) do
    days = Date.diff(Date.utc_today, person.birthday)
    days / 365.25
  end

  def home(%Person{} = person) do
    %{person | location: "home"}
  end

  def away(%Person{} = person) do
    %{person | location: "away"}
  end
end

One final thing to do here is to use pattern matching to pull out the values from the struct that we depend on. Let's change the two functions of full_name and age to this:

def full_name(%Person{first_name: first_name, last_name: last_name} = person) do
  "#{first_name} #{last_name}"
end

def age(%Person{birthday: birthday} = person) do
  days = Date.diff(Date.utc_today, birthday)
  days / 365.25
end

Public and private functions

Before we move on from here, there's one extra concept I would like to share with you. That concept is about public and private functions in modules. Sometimes, we will have functions in modules that we will not want to share with the outside world. Those functions can be kept private so that only other functions inside the module know about it.

Let's say that instead of having two functions called home and a away, we instead wanted to have a function called toggle_location that toggled the person's location between "home" and "away"?

Well, here's how we might write that function:

def toggle_location(%Person{location: "away"} = person) do
  %{person | location: "home"}
end

def toggle_location(%Person{location: "home"} = person) do
  %{person | location: "away"}
end

And now we can compile the module once again, and use this function:

iex> c "person.exs", "."
iex> person = %Person{
  first_name: "Izzy",
  last_name: "Bell",
  birthday: ~D[1987-12-04],
}
iex> person |> Person.toggle_location
%Person{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: "away"
}

This function does exactly what our home and away functions do, and so we can remove those functions.

But this toggle_location function is public -- it's accessible outside of the module still -- and weren't we talking about both public and private functions? You're right! We were. Let's get to that now.

The two function clauses of toggle_location look remarkably similar. They both set a location key to a particular value. This is a clear opportunity for tidying up some of our code, and it's a great opportunity to demonstrate private functions too.

Let's add a new function -- a private function to our module. We add private functions to the bottom of our module, and define them with defp, where the "p" stands for private.

defp set_location(%Person{} = person, location) do
  %{person | location: location}
end

Now back up in toggle_location, we can use this function to set the location:

def toggle_location(%Person{location: "away"} = person) do
  person |> set_location("home")
end

def toggle_location(%Person{location: "home"} = person) do
  person |> set_location("away")
end

This way, the code involved with setting the location can be shared across these toggle_location functions, and any other functions that later on might also set a location. Perhaps there'll come a time where we might want to announce what a particular person's location is each time it changes:

defp set_location(%Person{} = person, location) do
  IO.puts "#{person |> full_name}'s location is now #{location}"
  %{person | location: location}
end

The private function is an ideal place to put that code. It centralises the code in one simple place, and hides internal implementation details about how a location is set.

Structs are maps at heart

There's one last thing to cover on the topic of structs. Structs are maps when you get to the bottom of things. Structs can be passed as the argument to any Map function, like Map.get/2 for instance:

iex> %Person{} |> Map.get(:location)
"home"

This is because the underlying implementation of structs is based on maps. We can see this in action if we call the Map.keys/1 function on a struct:

iex> %Person{} |> Map.keys
[:__struct__, :birthday, :first_name, :last_name, :location]

The Map.keys/1 function returns not just the four keys that we've defined with defstruct, but a fifth key called :__struct__. This key contains the module name of the struct. We can see this by asking the struct for its __struct__ key's value:

iex> %Person{}.__struct__
Person

Structs are, simply put, maps with one extra key: a :__struct__ key. We can even define a map ourselves with such a key to prove this:

iex> %{__struct__: Person, first_name: "Izzy", last_name: "Bell", birthday: ~D[1987-12-04], location: "home"}
%Person{
  birthday: ~D[1987-12-04],
  first_name: "Izzy",
  last_name: "Bell",
  location: ""
}

Structs are maps at heart!

Structs and Protocols

There's been one other type of struct that we've been using a lot in this chapter, and we haven't even talked about it being a struct! It's this:

~D[1987-12-04]

Izzy exclaims: "That's not a struct! There's no percent sign!". Yup, there's not a single percent sign there. But it's still a struct! How can we tell? We can use Map.keys/1:

iex> ~D[1987-12-04] |> Map.keys
[:__struct__, :calendar, :day, :month, :year]

This function returns a list of keys, and that list contains :__struct__, and that's how we know that dates are structs under the hood.

Izzy is right that structs usually contain a percent sign. When we create a date, we don't use a percent sign to create it. Instead, we use the ~D sigil. Similarly, when a date is displayed (like in some output for our terminal) it is not displayed like this:

%Calendar.Date{calendar: Calendar.ISO, day: 4, month: 12, year: 1987}

Instead, it is shown like this:

~D[1987-12-04]

This is due to some code within Elixir itself. This code sees that a date is about to be output, and instead displays it in this condensed format instead for readability.

This feature of Elixir is called a protocol, and in particular this is the Inspect protocol we're talking about here. When a date is output on the screen, Elixir checks if there is an inspect protocol implemented for dates. There is, and so it gets used instead of the regular struct output.

That description is a little wordy, so let's see some code of our own in action! Let's go into our person.exs file and define an implementation for this Inspect protocol:

defmodule Person do
  # ...
  # functions go here
  # ...

  defimpl Inspect do
    def inspect(%Person{
      first_name: first_name,
      last_name: last_name,
      location: location,
      }, _) do
      "Person[#{first_name} #{last_name}, #{location}]"
    end
  end
end

Then let's re-compile this module back in iex:

iex> c "person.exs", "."
[Inspect.Person, Person]

Note that this has now compiled two modules: Inspect.Person and Person. The Inspect.Person module has been automatically generated and it will be used when a person struct is inspected.

To inspect a person struct, all we need to do is to generate one and get the console to do the actual inspection:

iex> %Person{first_name: "Izzy", last_name: "Bell"}
Person[Izzy Bell, home]

Great! This is now working. This has allowed us to condense the information that is displayed when using a Person struct in the console. This can be useful if you want to limit the amount of data that is shown in the terminal and just something I thought you should know about before we wrap up this chapter on modules and structs!

15. Part 3: Recap

We now come to the end of Part 3.

This part of Joy of Elixir was all about exploring the functions and other things that Elixir provides us.

In this part, we introduced built-in modules. These are collections of functions that help us work with certain data types in Elixir. We saw the String, List, Map, Enum and File modules.

We can chain these functions together using the pipe operator, as we saw back in Chapter 10. Here's an example of us chaining together two functions from the String module that we saw during that introduction:

iex> "hello pipe operator" |> String.upcase() |> String.reverse()

The pipe operator is one of Elixir's best features as it allows us to read the code from left-to-right, just as we would be reading this sentence.

We should now have a good understanding around the fundamental building blocks that Elixir provides us. We can work with strings, lists, maps and files now, and we know where we can find these functions too.

In Chapter 12 we looked at conditional statements as a way of deciding when to run a particular piece of code. We saw how we could pattern match on the function responses with case, use cond to check conditions and run code depending on that condition, and we used if and unless to perform similar checks to cond. Finally, we wrapped up by covering with, which runs code in the order we specify it in, and will only execute code if multiple conditions are met.

In Chapter 13 we saw where we could find additional functions, or information about functions. We saw that IEx provides a helper called h that works like this:

iex> h Map.get/2

This then returns the documentation for the Map.get/2 function:

def get(map, key, default \\ nil)
Gets the value for a specific key in map. If key is present in map with value value, then value is returned. Otherwise, default is returned (which is nil unless specified otherwise). ## Examples
iex> Map.get(%{}, :a) nil iex> Map.get(%{a: 1}, :a) 1 iex> Map.get(%{a: 1}, :b) nil iex> Map.get(%{a: 1}, :b, 3) 3

In this same chapter, we learned about navigating through IEx prompts using the arrow keys on the keyboard, as well as the v helper function in IEx that gives us the last value, or we could use v(3) to get the value from the third line of IEx.

Finally in Chapter 13, we saw that there are two great documentation sites, the official documentation and Elixir School. You can use both of these resources to supplement your learning when you're reading this book.

In Chapter 14 we got to see how to create our own modules and structures (or "structs" for short). Creating modules allows us to group together similar functions, just like what is done for the String and Map built-in functions for Elixir.

In the final part of this book, we're going to be looking into how we can build an Elixir project just like the pros do by using a tool called Mix. We'll be looking at how we can start a new Mix project, bring in other people's code into our project, and then finally look at how we can ensure the code that we write is always working through the process of writing automated tests for that code.

... And the rest

Appendix A: Setup and Install

The way you install Elixir is different on each operating system. It would be very nice if we could all agree on the One True Way™ to install software, but alas, we do not. So below are the three major operating systems and their installation instructions, for your convenience.

Code Editor

In this book you're going to need to have something to edit code with. I recommend Visual Studio Code because it is easy-to-use and works across all the major operating systems. You should install it now.

macOS

There's some official instructions on how to install Elixir but they're missing one key step: installing either Homebrew or Macports first.

Homebrew and Macports are tools that help installing software packages on macOS. Homebrew is the more modern option, and so that's the one I would really recommend using here.

To install Homebrew, copy the line from Homebrew's homepage into your Terminal and run it. If you don't know where to find your Terminal, it's located in the Applications directory → Utilities → Terminal.

Once you've run that command to install Homebrew and it has finished, then run this one to install Elixir:

brew install elixir

Windows Install

Windows is by far the easiest to install Elixir on. The official Elixir install guide recommends using the Elixir installer. All you have to do is to download it, click next a couple of times and then finish once and Elixir should be installed.

Linux Install

There are too many varieties of Linux to list here, so instead I will link to the official Elixir installation instructions for Linux. Follow those to get Elixir installed on your Linux operating system.

Verifying Elixir is installed

No matter how you installed Elixir, verifying it is installed is done the same way across all operating systems. To verify that Elixir was installed correctly, run this command:

elixir -v

If this command says something like this:

Erlang/OTP 23 [erts-11.1.1] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [hipe] [dtrace]

Elixir v1.10.4

Then that means Elixir has been successfully installed.

Buzzwords explained

In this chapter, we'lll dissect what that darned Wikipedia description meant. You know the one. The one that perplexed us back in "Elixir? Isn't that something you drink?".

That darn Wikipedia description

Let's take another look at that description:

Elixir is a functional, concurrent, general-purpose programming language that runs on the Erlang virtual machine (BEAM).

Yeah, it's still not quite clear, is it? How about if we did this:

Elixir is a functional, concurrent, general-purpose programming language that runs on the Erlang virtual machine (BEAM).

This is better because we have each concept neatly isolated from all the other concepts. Let's go through them one by one.

Functional

When we talk about Elixir being a "functional" language we don't mean to call it that because it operates correctly; using one of the traditional meanings of the word "functional". We don't mean that it "designed to be practical and useful, rather than attractive" — to quote the macOS dictionary &mdahs; although it certainly is all three of those things.

What is meant by this is that Elixir does what it does with a heavy reliance on functions. To understand how functions work is to understand how Elixir works. If you've read Chapter 5: Funky Functions (or further) already, you will have seen these in action. Here's the first example from that chapter:

iex> greeting = fn (place) -> "Hello, #{place}!" end
#Function<6.52032458/1 in :erl_eval.expr/5>

In this example, a function is defined and assigned to the variable called greeting. This function performs a certain action here, in this case that action is to generate a string beginning with "Hello" and then finishing with a place, which we provide to the function when we run the function.

Elixir doesn't do anything with the function until we run some other code to tell it to.

iex> greeting.("World")
"Hello, World!"

When we run the greeting and pass it the string "World" it combines the string from inside the function and the string passed as an argument into one string and then returns that.

This is just one little example of how Elixir uses functions to work. Another example is the built-in functions that we show first in Chapter 8: Strings, input, and output:

iex> String.reverse("reverse this")
"siht esrever"

In this example, we're running the reverse/1 function from the String module. Elixir has some code already defined that knows how to perform the task of reversing a string. All we need to do to use it is to specify the function and the required arguments and Elixir handles the rest.

These are just two small examples of why Elixir is called a functional language. It is called a functional language because you use functions to make it perform actions.

Concurrent

When Elixir is called a "concurrent" language, it means that it can run multiple parts of a program at the same time. A good description of concurrent computing can be found on Wikipedia.

The advantage of this is that Elixir can be used to perform multiple operations all at the same time. Let's think of a program that needs to take a list of accounts and their transactions for a bank, and calculate the balances for each of the accounts. Think of a really, really long list full of transactions and accounts. A list that might look like this:

  • Account 1 - Transaction 1 - $10
  • Account 1 - Transaction 2 - $20
  • Account 2 - Transaction 1 - $30
  • ...

A non-concurrent program may read this list one item at a time, and process it that way using only a single part of a computer (called a "core") to do the processing.

Elixir's concurrency features can be used to split reading this list into smaller tasks that can then be performed all at the same time. This will ultimately rely on multiple cores of the computer to do the reading and calculation, and will result in a much faster calculation than using only a single core.

General-purpose programming language

A general-purpose programming language is one that is not designed for a particular purpose, but it can be used across all kinds of purposes. In relation to Elixir, this means you can use Elixir to create all sorts of programs that do whatever you can imagine.

Erlang virtual machine (BEAM)

The Erlang virtual machine (BEAM) is the foundation of Elixir's power. It's a rock-solid foundation that has existed existed since 1986. Erlang provides the underlying concurrency features for Elixir, managing the low-level things that tell the computer how to schedule particular tasks in the most efficient way possible.