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!

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.

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.

 
595
Kudos
 
595
Kudos