Joy of Elixir

5. Funky functions

After about half an hour, Roberto re-appears from seemingly nowhere. His computer screen glows with the information he has collected from the crowd. We're not quite sure how long he's been back for, but the moment our attention fixes on him, he 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 Roberto, 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 moreso 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

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?", Roberto asks keenly, suddenly impressed about the computer's ability to remember functions. I'm sure you're asking the same thing, dear reader.

Well, Roberto (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 Celcius unit to the wacky Fahrenheit equivalent:

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

The mathematical equation to convert celcius 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 celcius, multiply (*>) it by 1.8, and then add (+) 32.

Let's run our function with some celcius 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 celcius 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 a #{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.("Roberto", "Male", "30ish")
"Hello, Roberto! I see you're a Male 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.("Roberto")
** (BadArityError) #Function<18.52032458/3 in :erl_eval.expr/5>⏎
  with arity 3 called with 1 argument ("Roberto")

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'?", Roberto asks, clearly flummoxed (and perhaps a bit affronted) by the word. Unlike the computer, Roberto is not brutally indifferent when it comes to these things. After all, Roberto thought he 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.

Exercises

  • Make a function which turns fahrenheit temperatures into celcius.
  • 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 either of these two solutions in their own file and run them through the elixir command-line tool.