Function currying in Elixir
Curry? #
Currying is a technique that translates a function of arity N into a sequence of functions that all have the arity 1. Function arity means the number of arguments a function takes.
# The add function has the arity of 2
add = fn x, y -> x + y end
The term curry/currying was coined back in the 60s referring to the logician Haskell Curry. The programming language named after him has curried functions built in, and many other languages have implemented the same kind of behavior via libraries.
Implementing currying in Elixir #
Elixir is a functional language, but unlike Haskell, it does not have built in function currying.
In Haskell we could do this
let add x y = x + y -- add is curried by default
let increment = add 1
increment 3 -- returns 4
A naive approach #
Elixir functions are first class, so your function can return them and receive them as values.
add = fn x ->
fn y -> x + y end
end
And our add function can be used like the Haskell version
increment = add.(1)
increment.(3) # => returns 4
This works, but its obvious that we want something more general, and reusable. In fact the above could be considered a plain function that is partially applied.
We can do better #
Again, Elixir being a fantastic language has tools we can use to make a more general solution. To get the job done we will use pattern matching, and recursion.
In the first paragraphs we talked about arity. Arity is very important in Elixir and Erlang. The language actually considers functions with the same name, but with different arity completely different functions. This is why you see a slash followed by an integer e.g. some_function/2
in various places in the language documentation.
Lets build our own curry function #
curry/1 #
The first function our curry implementation needs is the starting point, here our function has the arity of 1.
The :erlang
atom lets you call Erlang code (calls are zero cost), here we gather information about the function that is to be curried, using pattern matching we only care about the second returned value, omitting the first with _
so the compiler wont yell at us for unused variables. Finally we call the curry function again, but this time with a different arity.
def curry(fun) do
{_, arity} = :erlang.fun_info(fun, :arity)
curry(fun, arity, [])
end
curry/3 #
This time our curry function has the arity of 3. We receive the function that is to be curried, the function arity and arguments. The arguments is just a empty list in the first call.
This function will return a function, taking an argument arg
and recurse on itself decrementing the variable arity by one. The arity
variable is often called an accumulator, and is widely used in recursive functions. Finally the last argument that was originally an empty list will be filled with the supplied argument.
def curry(fun, arity, arguments) do
fn arg -> curry(fun, arity - 1, [arg | arguments]) end
end
curry/3 #
Stop! Wasn’t functions in Elixir supposed to distinguished by name and arity? So how can Elixir distinguish two functions with the same name and the same arity?
To find out we can test how Elixir calls functions
defmodule Callme do
def im_unique {:unique, "callme"} do
IO.puts "Ring ring"
end
def im_unique {:unique, "callyou"} do
IO.puts "No answer"
end
end
Callme.im_unique {:unique, "callme"} # => Ring ring
Both functions have the same name, same arity and even accepts the same type of input. Still there a small difference, the tuple is not the same. So we can conclude that pattern matching is used in function invocation.
Lets build the final piece of our curry function.
When the second curry function has recursed as many times as the original function that we wanted to curry had as arity the pattern no longer matches, and Elixir will match the last curry function. The arity parameter is now down to 0
and we can apply the original function to the arguments.
Because we have recursed the parameters we will need to reverse the list so we dont mess up the original order of the parameters the user has passed in. The fun
function will have the same arity as the number of items collected in the arguments list.
def curry(fun, 0, arguments) do
apply(fun, Enum.reverse arguments)
end
```
##Putting it all together
Lets have the code in one place, and test it out!
```elixir
defmodule Curry do
def curry(fun) do
{_, arity} = :erlang.fun_info(fun, :arity)
curry(fun, arity, [])
end
def curry(fun, 0, arguments) do
apply(fun, Enum.reverse arguments)
end
def curry(fun, arity, arguments) do
fn arg -> curry(fun, arity - 1, [arg | arguments]) end
end
end
```
Lets see how to use our curry function.
```elixir
import Curry
curried = curry(fn a, b, c, d -> a * b + div(c, d) end)
# => #Function<0.122306914/1 in Curry.curry/3>
# We get a function back!
# Lets apply some params!
curried.(5).(5).(10).(2)
# => 30
# We get a result when all params are passed!
five_squared = curried.(5).(5)
five_squared_plus_five = five_squared.(10).(2)
# => 30
# We get a result!
```
## A few more examples
By currying functions you can build small reusable functions that are really simple to test, and reason about.
defmodule Curried do
import Curry
def match term do
curry(fn what -> (Regex.match?(term, what)) end)
end
def filter f do
curry(fn list -> Enum.filter(list, f) end)
end
def replace what do
curry(fn replacement, word ->
Regex.replace(what, word, replacement)
end)
end
end
Match when there is a space #
has_spaces = Curried.match(~r/\s+/)
Reuse has_spaces in our filter #
sentences = Curried.filter(has_spaces)
Censor the bad things #
disallowed = Curried.replace(~r/[jruesbtni]/)
Insert censor token #
censored = disallowed.(“*”)
Pass the arguments to the curried function #
allowed = sentences.([“justin bibier”, “and sentences”, “are”, “allowed”])
=> returns [“justin bibier”, “and sentences”] #
Pipe the first sentence and apply the censor #
allowed |> List.first |> censored.() # => returns “****** ******”
As you see currying makes it very simple to reuse functions, and build up higher order functions. In Elixir its even more elegant than in most other languages because you can use the pipe operator to chain functions.