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 functionfun
. Then, we pass the control to the functionCurry.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 functionfun
with the list of argumentsargs
.
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.