Joy of Elixir

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.