Milton Mazzarri
Milton Mazzarri
5 min read

Tags

NOTE: This article is a follow-up examination after the blog post Function currying in Elixir by @stormpat

In his article, Patrik Storm, shows how to implement function currying in Elixir, which could be really neat in some situations. For those who haven’t read Patrik’s post, first, let us clarify what is function currying.

Currying is the process of transforming a function that takes multiple arguments (arity) into a function that takes only one argument and returns another function if any arguments are still required. When the last required argument is given, the function automatically executes and computes the result. As a first step, let us apply function currying manually:

iex(1)> greet = fn greeting, name -> IO.puts "#{greeting}, #{name}" end
#Function<12.52032458/2 in :erl_eval.expr/5>
iex(2)> greet.("Hello", "John") # uncurried function
Hello, John
:ok
iex(3)> greetCurry = fn greeting -> fn name -> IO.puts "#{greeting}, #{name}" end end
#Function<6.52032458/1 in :erl_eval.expr/5>
iex(4)> greetCurry.("Hello").("John")
Hello, John
:ok

To get a general solution, Patrik uses a nice approach that combines pattern matching and tail-call optimization, let’s dive into his implementation:

# file: curry.exs
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

The main points in this Curry module are the following:

  • Curry.curry/1 represents our entry point, this function use :erlang.func_info/2 to know the arity (number of arguments) of the given function fun. Then, we pass the control to the function Curry.curry/3
  • The recursive function Curry.curry/3 will return anonymous functions that only takes just one argument.
  • When the last required argument is given we will use Kernel.apply/2 to invoke the given function fun with the list of arguments args.

Let’s show how we can use function currying, I’ll use the same examples that Patrik did in his post but using ExUnit instead:

# file: curried.exs
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

Our unit tests:

# file curry_test.exs
ExUnit.start()

Code.require_file("curry.exs", __DIR__)
Code.require_file("curried.exs", __DIR__)

defmodule CurryTest do
  use ExUnit.Case

  test "applying all the params at once or one step at a time should produce same results" do
    curried = Curry.curry(fn a, b, c, d -> a * b + div(c, d) end)
    five_squared = curried.(5).(5)

    assert five_squared.(10).(2) == curried.(5).(5).(10).(2)
  end

  test "curry allow to create composable functions" do
    has_spaces = Curried.match(~r/\s+/)
    sentences = Curried.filter(has_spaces)
    disallowed = Curried.replace(~r/[jruesbtni]/)
    censored = disallowed.("*")

    allowed = sentences.(["justin bibier", "and sentences", "are", "allowed"])

    assert "****** ******" == allowed |> List.first() |> censored.()
  end
end

Now we can run our tests as follows:

$ elixir curry_test.exs
..

Finished in 0.2 seconds (0.2s on load, 0.00s on tests)
2 tests, 0 failures

Randomized with seed 604000

It is working, but I feel we can improve a few things, in this case, our curry function only takes into account that the arguments are given from left to right. What about if we want to give the parameters from right to left? Let’s introduce curryRight:

# file: curry.exs
defmodule Curry do
  def curry(fun) when is_function(fun), do: curry(fun, :left)

  def curryRight(fun) when is_function(fun), do: curry(fun, :right)

  defp curry(fun, direction) do
    {_, arity} = :erlang.fun_info(fun, :arity)
    curry(fun, arity, [], direction)
  end

  defp curry(fun, 0, args, :left) do
    apply(fun, Enum.reverse(args))
  end

  defp curry(fun, 0, args, :right) do
    apply(fun, args)
  end

  defp curry(fun, arity, args, direction) do
    &curry(fun, arity - 1, [&1 | args], direction)
  end
end

Then, our Curried module, which holds support functions, is much simpler if we do the following:

# file: curried.exs
defmodule Curried do
  import Curry

  def match(term), do: curry(&Regex.match?/2).(term)

  def filter(f), do: curryRight(&Enum.filter/2).(f)

  def replace(what), do: curry(&Regex.replace(&1, &3, &2)).(what)
end

Now, without any change in our unit tests, we can verify that everything is working as before.

$ elixir curry_test.exs
..

Finished in 0.2 seconds (0.2s on load, 0.00s on tests)
2 tests, 0 failures

Randomized with seed 561000

Do we need to apply curry to everything?

No, it will always depend of your case, first, let’s see how worse can be if apply currying manually and then we will try to find another way to this whole process as a data transformation workflow.

# file: manual_currying.exs
defmodule ManualCurrying do
  def match(term) do
    fn what -> Regex.match?(term, what) end
  end

  def filter(f) do
    fn list -> Enum.filter(list, f) end
  end

  def replace(what) do
    fn replacement ->
      fn word ->
        Regex.replace(what, word, replacement)
      end
    end
  end
end

Our unit tests:

# file manual_currying_test.exs
ExUnit.start()

Code.require_file("manual_currying.exs", __DIR__)

defmodule MunualCurryingTest do
  use ExUnit.Case
  import ManualCurrying

  test "applying all the params at once or one step at a time should produce same results" do
    curried =
      fn a ->
        fn b ->
          fn c ->
            fn d ->
              a * b + div(c, d)
            end
          end
        end
      end

    five_squared = curried.(5).(5)

    assert five_squared.(10).(2) == curried.(5).(5).(10).(2)
  end

  test "curry allow to create composable functions" do
    has_spaces = match(~r/\s+/)
    sentences = filter(has_spaces)
    disallowed = replace(~r/[jruesbtni]/)
    censored = disallowed.("*")

    allowed = sentences.(["justin bibier", "and sentences", "are", "allowed"])

    assert "****** ******" == allowed |> hd() |> censored.()
  end
end

But, if you just one to execute this just one time, maybe we can do better thinking everything as a data transformation workflow, and actually this is the more succint way:

"****** ******" ==
  ["justin bibier", "and sentences", "are", "allowed"]
  |> Enum.filter(&Regex.match?(~r/\s+/, &1))
  |> hd()
  |> String.replace(~r/[jruesbtni]/, "*")

Wrapping up

Function currying is an interesting technique that allow us to reuse functions, for example, we can create a module with small functions that behave consistently without so much effort. Although, we need to keep in mind the arguments order when we want to apply function currying. Sometimes for functions like Enum.map/2, Enum.reduce/2, Enum.filter/2, etc. it would be better or easier to use curryRight than curry, normally our decision will depend on the arguments that will change constantly, because we want to put those at the end of the execution path.

As a final note, it could be a interesting exercise to implement uncurry, which is a function that converts a curried function to a function with arity n, that way we can convert these two types in either direction.

References