Function composition is a technique used to build complex functions out of simpler ones. Elixir does not provide a mechanism for composing two functions into a new function. Let’s play with that a little and see what can be done.
Say using Elixir you want a function that appends an element to a list. An efficient way to do this is by first reversing the list, then prepending the element, and finally reversing the list again.
Kick things off by manually implementing
append in terms of reversal and prepending:
iex> append = fn list, item -> list = Enum.reverse(list) list = [item|list] Enum.reverse(list) end #Function<...> iex> append.([1,2,3], 4) [1, 2, 3, 4]
This certainly gets the job done, but perhaps it could be even more to the point. The main problem is the implementation shifts the focus from the operations to state management. The
list variable is introduced and repeatedly referenced (5 times!) during the execution of the function. As Josh points out in his decomposition blog, Elixir provides a mechanism for composing function applications with its pipe
|> operator. Refactor it with pipes:
iex> prepend = &[&2|&1] #Function<...> iex> append = fn list, item -> list |> Enum.reverse |> prepend.(item) |> Enum.reverse end #Function<...> iex> append.([1,2,3], 4) [1, 2, 3, 4]
That does seem quite a bit better (idiomatic, even)! You no longer repeatly refer to
list, and the implementation is more clearly composed of
prepend/2 between calls to
reverse/1. Even so, the state management is still present in the form of arguments. Is there an implementation that’s even more clearly composed of the operations? What if you could compose the existing functions together into a new
Elixir does not provide such an operation, but imagine a custom infix operator,
<|>, for composing functions:
iex> append = reverse <|> prepend <|> reverse #Function<...>
append function would, in effect, capture the expression
reverse(prepend(reverse(list), item)), requiring the arguments
item in that order.
To arrive at the final implementation, start by implementing a
compose/2 function using recursion. First define the base case:
defmodule Compose do def compose(f, arg) do f.(arg) end end
This base case effectively applies the
arg to the function
f. For example:
iex> double = fn n -> n*2 end #Function<...> iex> Compose.compose(double, 4) 8
Next add an implementation of
compose/2 that recurses when the second argument is a function:
defmodule Compose do def compose(f, g) when is_function(g) do fn arg -> compose(f, g.(arg)) end end def compose(f, arg) do f.(arg) end end
This version of
compose/2 returns a function that applies its argument to
g and then composes the result with
f. However, at this point the implementation works only to compose functions that accept a single argument, i.e. having an arity of 1:
iex> reverse_sort = Compose.compose(&Enum.reverse/1, &Enum.sort/1) #Function<...> iex> reverse_sort.([3,1,2]) [3, 2, 1]
It does not work for functions requiring many arguments (N-arity):
iex> reverse_prepend = Compose.compose(&[&2|&1], &Enum.reverse/1) #Function<...> iex> reverse_prepend.([1,2,3]) ** (BadArityError)
The error happens, because
compose/2 only ever applies one argument, i.e.
.(arg). Elixir is strict about the arity of functions. For
compose/2 to work with N-arity functions, you need some way to apply a variable number of arguments.
A solution to this problem is changing how N-arity functions are applied. Since there is no way to anticipate how many arguments will be needed, you can instead rearrange the function to support multiple single-argument applications until all have been provided. This is a common functional programming technique known as currying.
Currying converts a function of arity N into a function of 1-arity that when applied N times produces the result. Consider this example of manual currying with nested functions:
iex> add = fn a -> fn b -> a+b end end #Function<...> iex> add_one = add.(1) #Function<...> iex> add_one.(2) 3
add is defined to return another function. The result of applying the value
add returns a function that accepts the second argument for the addition. This idea of applying only part of what a function needs to return a result is called partial application. The partial application of
1 is a function that “adds one”. Once all the arguments have been applied, the result is returned:
iex> add.(2) #Function<...> # a function that "adds 2" iex> add.(2).(3) 5
Notably, this mechanism is not built into Elixir, but there are packages that add the behavior as well as a great post on currying in Elixir. For the remainder of this post, it’s assumed you have a module
Curry that includes a function
curry/1 such that you can:
iex> add = Curry.curry(fn a,b -> a+b end) #Function<...> iex> add.(2).(5) 7
As you have learned, currying is a solution to your variable argument problem. It also happens to fit really well into the recursion that is already set up in
compose/2. Update the implementation to curry both functions passed in:
defmodule Compose do + import Curry + def compose(f, g) when is_function(g) do - fn arg -> compose(f, g.(arg)) end + fn arg -> compose(curry(f), curry(g).(arg)) end end def compose(f, arg) do f.(arg) end end
It might be surprising to realize this is the only change needed to complete the implemetnation! The recursive definition of
compose/2 applies arguments one at a time to the composed function until a result is found, and then then applies that result to the outer function in the base case. See if it fixed your arity error:
iex> reverse_prepend = compose(&[&2|&1], &Enum.reverse/1) #Function<...> iex> reverse_prepend.([1,2,3]).(4) [4, 3, 2, 1]
Nice! Notice that each successive argument is a partial application to the underlying curried functions. The really cool thing is that the order of the arguments called on
reverse_prepend matches the order of respective arguments to each composed function; the list first, then the prepended item.
For convenience, complete your implementation of
Compose with a custom infix composition operator,
defmodule Compose do import Curry def f <|> g, do: compose(f, g) ... end
That’s it! Now by importing
Compose, functions may be composed together with
iex> import Compose Compose iex> square = fn n -> n*n end #Function<...> iex> fourth = square <|> square #Function<...> iex> fourth.(2) 16
Tie up this experiment by returning to the original task. Recall, you want to define a function
append in terms of
prepend. The implementation with function composition is now purely expressed as operations:
iex> reverse = &Enum.reverse/1 #Function<...> iex> prepend = &[&2|&1] #Function<...> iex> append = reverse <|> prepend <|> reverse #Function<...> iex> append.([1,2,3]).(4) [1, 2, 3, 4]
One important note is that the Elixir implementation demonstrated is not pure function composition in the mathematical sense. Function composition requires that function signatures match exactly in order to be compatible for composition. No such restrictions exist in this implementation. In fact it displays the interesting property of trickling arguments down in order until each composed function is fully applied. Elixir is a dynamically typed language, and as such it allows a lot of flexibility in how functions are defined and applied to. Have fun!
Interested in learning more about our basic and advanced Back-End Courses?
Learn from the experts at a Big Nerd Ranch Bootcamp!
Interested in leveling up your coding skills from the same authors of the Big Nerd Ranch Guide? Subscribe to The Frontier today!