Erlang Central

Functionally Programming in Elixir

From ErlangCentral Wiki

Author: tee.teoh@erlang-solutions.com

Functionally Programming in Elixir Part 1

  How do you solve problems in Elixir without a Cauldron

Decidably every programming language was developed around a particular paradigm. The paradigm guides (some say forces) the programmer to take design and implementation decisions in solving a problem.

Elixir is called a functional concurrent language and both principles interplay in most programming projects.

When programming in the small Elixir's functional roots comes into play. However when programming in the large (or colossal, G. Booch) Elixir's concurrent nature is paramount. To be fair it is the OTP principles built over Erlang which is leveraged.

This topic will be divided into 2 major parts: Functional Elixir and Concurrent/OTP Elixir.

Functional Elixir

  1. 1. The name as the hint

Functional languages uses functions as a basic building block and Elixir is no different. But what is a function ?

A function relates some input with some output. In a way a function describes the relationship between the input and output. For easy reference we typically name this relationship, however it is important to note that this is not required.

Lets define the "the relationship between John and 24 is age", "the relationship between Mary and 21 is age" and "the relationship between Susy and 25 is age"

def age('John'), do: 24
def age('Mary'), do: 21
def age('Susy'), do: 25

In Elixir speak "the definition of age for John is 24", "the definition of age for Mary is 21" and "the definition of age for Susy is 25".

Another example of a function:

def square(x), do: x * x

The 'x' here refers to any input. It is a place holder which is later use in describing the relationship. In the relationship "square" the input is multiplied with itself.

  1. 2. Anonymous function

Recall the naming of a function is optional. We could have defined the "square" relationship as:

fn(x) -> x * x end

Since the function is not named we call this an anonymous function. Let us write the anonymous equivalent for "age"

fn
  ('John') -> 24
  ('Mary') -> 21
  ('Susy') -> 25
end

Of course being anonymous makes it a tad tricky to use. Once defined you can bind the definition to a name for later reference.

anon_square = fn(x) -> x * x end

To use it later call "anon_square" with the "." operator. E.g.

anon_square.(4)
  1. 3. Domain, Codomain, Range and Type

In Mathematic speak the inputs which a function is defined for is called the domain, all possible outputs of the function the codomain and the specific outputs of the function its image.

In Elixir we just say input and output. Elixir is non-pretentious like that.

What is important is the range of a function, in particular it's type.

Elixir has a very small set of types: integers, floating point numbers, atoms, ranges, regular expressions, pids, ports, references, tuples, lists, maps, binaries and functions.

Wait. What ? Functions ? Yes Elixir functions can return functions. We will talk about this later.

  1. 4. Function composition

When you have the type of a function's image similar to the input of another function you can do something called function composition. Function composition is combining two functions to build a more complicated function.

From the examples above the range of the "age" function has the same type as the input of the "square" function. Namely type integer, allowing us to combine both together.

In Elixir we have something similar called the pipe operator which looks like |> Granted the pipe is similar but not equivalent to function composition but it is there lets smoke it. [1]

age |> square

Let us put this into another function.

def square_age(name), do: name |> age |> square

In English "the relationship between name and it's age squared is the composition of its name age and square".

Notice how we use the "name" as input in the pipe above. Anything on the left hand side with the same type as the first parameter of the right hand side of the pipe can be placed together. Sort of like interlocking jig-saw puzzles.

The "square_age" function took a list type, 'John' is a specific list of integers commonly called a string, and converted it into an integer type.

This type conversion via functional composition is a fundamental functional programming technique. Repeat after me: type conversion via functional composition is a fundamental functional programming technique.

In English: get some stuff and massage it until we have the stuff we want.

  1. 5. Putting it together

This section puts the examples above into a single unit. Elixir requires functions to reside in a module. You can say a module is a collection or a set of functions that related in some way.

Start iex

$> iex

and in the iex shell type the following:

defmodule Efp do
  def age('John'), do: 24
  def age('Mary'), do: 21
  def age('Susy'), do: 25

  def square(x), do: x * x

  def square_age(name), do: name |> age |> square
end

You can now use it:

iex(2)> Efp.square 45
2025
iex(3)> Efp.square_age 'John'
576
  1. 6. Collections

At this point our function took a singular item and transformed it into another singular item.

Elixir has four collection types: tuples, lists, maps and binaries. Tuples, maps and binaries can be treated as a singular items as they tend to be collective units that aren't broken up.

E.g. The tuple

{'John', 24, 'Spokane'}

is treated as a unit as is the map

%{'name' => 'John', 'age' => 24, 'city' => 'Spokane'}

as is the binary

"John;24;Spokane"

Lists however are a whole different story. List consist of items we want to transform individually.

  1. 7. List transformation: map

The simplest transformation is from one list to another. For example a list of names to a list of age

['John', 'Susy'] -> [24, 25]

In functional programming and Elixir we call this a map, a mapping from one list to another.

Conveniently there is a "map" function defined in the Enum module. "Enum.map" takes a collection and a function. Since we have the function "age" defined we can use it via the "&" use function operator.

iex(4)> ['John', 'Susy'] |> Enum.map(&Efp.age/1)
[24, 25]

We read

&Efp.age/1

as use the "age" function that takes 1 argument from the "Efp" module.

How would you achieve the following map? :

['John', 'Susy'] -> [576, 625]

where the output is the square of the person's age.

Provide two possible solutions.

Write a new module "EfpMap" which uses the functions in "Efp" but do the following mapping. Lets call the function "ages_names"

['John', 'Susy'] -> [{'John', 24}, {'Susy', 25}]
iex(5)> ['John', 'Susy'] |> EfpMap.ages_names
[{'John', 24}, {'Susy', 25}]

Naturally "EfpMap.ages_names" will use "Enum.map" and "Efp.age" internally.

  1. 8. List transformation: zip

Taking a input list and mashing it with the resulting output list happens quite often enough that we have a name for it, "zip". You can find "zip" in the "Enum" module.

['John', 'Susy'] x [24, 25] -> [{'John', 24}, {'Susy', 25}]
iex(6)> Enum.zip ['John', 'Susy'], [24, 25]
[{'John', 24}, {'Susy', 25}]
  1. 9. List transformation: filter

Filtering a list is another basic operation.

For example finding ages greater than 24

[24, 21, 25] -> [25]

As usual "filter" exists in the "Enum" module.

Let us use an anonymous function for this example:

iex(7)> [24, 21, 25] |> Enum.filter(fn(age) -> age > 24 end)
[25]

Now what if we have a list of names and want the name of the person older than 24 ?

Repeat after me: type conversion via functional composition is a fundamental functional programming technique.

['John', 'Susy', 'Mary'] -> [{'John', 24}, {'Susy', 25}, {'Mary', 21}] -> [{'Susy', 25}] -> ['Susy']

We have the first transformation, "EfpMap.ages_names", the second transformation is a filter and the third a map.

In a module "EfpFilter" define a function "gt24" that returns true when the age is greater than 24.

defmodule EfpFilter do
   def gt24({name, age}), do: age > 24
end

Notice how we decompose the tuple {name, age} in the function gt24 ? This is another example of pattern matching and it allows us to tease out the actual field we care about from a tuple.

Lets put what we current have together

iex(8)> ['John', 'Susy', 'Mary'] |> EfpMap.ages_names |> Enum.filter(&(EfpFilter.gt24/1))
[{'Susy', 25}]

Lastly the final step draws from pattern matching in "gt24".

iex(9)> ['John', 'Susy', 'Mary'] |> EfpMap.ages_names |> Enum.filter(&(EfpFilter.gt24/1)) |> Enum.map(fn({name,age}) -> name end)
['Susy']
  1. 10. Zippy filtering map: List comprehension

Phew! That is quite a bit of compositions going on. A short cut from all that composition is to use list comprehension. Personally I'm not fond of the Elixir list comprehension syntax, looks too different from the Mathematical comprehension but heh.

iex(10)> for name <- ['John', 'Susy', 'Mary'], Efp.age(name) > 23, do: name
['John', 'Susy']

In English, return the name of the person from the list such that their age is above 23.

Another example

iex(11)> for name <- ['John', 'Susy', 'Mary'], Efp.age(name) > 23, do: {Efp.age(name), name}
[{24, 'John'}, {25, 'Susy'}]

The first portion of a list comprehension is where the stuff comes from, the generator, the second part says which stuff we are interested in, the filter, and the final portion how the stuff is put together.

Although it is possible to have multiple generators and filters, multiple filters are more common. Each filter must evaluate to true before the final portion is formed.

iex(12) > for name <- ['John', 'Susy', 'Mary'], Efp.age(name) > 19, (name == 'John' or name == 'Mary'), do: name
['John', 'Mary']
  1. 11. List transformation: reduce

The last transformation we will discuss takes a list as an input and transforms it into a single output. For example the sum of squares, "sum_of_square" of all the people's ages.

We call such a transformation a "reduction". You might also come across the older term for it, a "fold".

[576, 441, 625] -> 1642

What we want to do is transform the list as such:

[576, 441, 625] -> 576 + 441 + 625

It is fair to say we can also write the above as:

[576, 441, 625] -> 0 + 576 + 441 + 625

As 0 is a valid initial value for the "sum_of_square". Using some parenthesis to make it easier to read we get:

[576, 441, 625] -> (((0 + 576) + 441) + 625)

where the result of the initial evaluation is use as partial input to the next just like a function composition.

Lets define an anonymous function which just adds two numbers and bind it to "sum".

iex(13)> sum = fn(x, y) -> x + y end

Now we have

[576, 441, 625] -> sum.(sum.(sum.(0, 576), 441), 625)

Lets reorder the parameters for sum so that the 2nd parameter will be the initial value and subsequent previously evaluated output:

[576, 441, 625] -> sum.(625, sum.(441, sum.(576, 0)))

Although this is not necessary it makes the next mental leap a mere hop.

Basically to use a "reduction" we have to provide it a list to operator on, an initial value and a function that is applied successively to the list.

As you might guess we find the "reduction" function in the "Enum" module hiding as "Enum.reduce"

iex(14)> [576, 441, 625] |> Enum.reduce(0, sum)
1642

Map, Filter and Reduce are the three main transformations you will encounter. The fourth common transformation is Reverse which, as you can guess, appears in the "Enum" module. Using Reverse is left as an exercise to the reader.

  1. 12. A little fib

When I first mentioned the map basic type I said we can treat it as a unit. This is partially true, a map can also be a collection where you want to process each individual (key, value) pair.

For example given a map:

%{'John' => 24, 'Mary' => 21, 'Susy' => 25}

we may want to find all individuals over 23. Unfortunately Elixir does not provide functions to operate on (key, value) pairs. We will have to first convert it to a list.

iex(15)> Map.to_list %{'John' => 24, 'Mary' => 21, 'Susy' => 25}
[{'John', 24}, {'Mary', 21}, {'Susy', 25}]

Which is in a familiar form we have worked with so far. You can take over from here...

To get back into a map use the "Enum.into" function like so:

iex(16)> Enum.into [{'John', 24}, {'Mary', 21}, {'Susy', 25}], %{}
%{'John' => 24, 'Mary' => 21, 'Susy' => 25}

Now, although Elixir does not have convenience functions that is not to say Erlang does not. You can find the equivalent Map and Reduce functions in the ":maps" module, although Reduce is called "fold".

Lets find the names of all individuals over 23 using ":maps.fold".

iex(17)> :maps.fold(fn
                      (key,value,acc) when value > 23 -> [key | acc]
                      (key,value,acc) -> acc
                    end, [], %{'John' => 24, 'Mary' => 21, 'Susy' => 25})
  1. 13. Summary

Repeat after me: type conversion via functional composition is a fundamental functional programming technique.

In this section we covered several transformation techniques demostrating how we can utilize it to solve problems functionally. Not once did we mention the naughty 'R' word and only briefly mentioned functions returning functions. Unfortunately the next section will be 'R'-rated.

Stay tuned.

Next in the Series

Footnote


[1] For those wondering, Torben Hoffmann pointed out that function composition returns a function, while pipes evaluate functions in series.