Re-implementing apply in Elixir

[boilerplate bypath=”fp-oo”]

Elixir already has a perfectly good apply . But FPOO suggests I try to write my own version, and why not?

def my_apply(func, sequence) do
  code = quote do
    unquote(func).(unquote_splicing(sequence))
  end
  {result, _} = Code.eval_quoted(code)
  result
end

This is considerably more verbose than the Clojure example in the book, without really seeming to add much expressiveness:

(def my-apply
  (fn [function sequence]
    (eval (cons function sequence))))

Perhaps an Elixir-experienced reader can show me a more idiomatic form. Although I think Elixir metaprogramming is likely always going to be wordier than Lisp because of a) its less-concise quoting/unquoting constructs; and b) the fact that the sexp-ish things that Elixir macros work on under the covers are more complex than their lisp counterparts. As an example, here’s the code-as-data version of adding two integers:

quote do: 1 + 2
# => {:+, [context: Elixir, import: Kernel], [1, 2]}

Whereas the lisp code-as-data representation of (+ 1 2) is, well… (+ 1 2). Though to be fair, that big ugly keyword list in the middle is optional metadata. The most minimal representation is:

{:+, [], [1, 2]}

…which is not that much bigger than the lisp version.

Actually, that gives me an idea. Let’s see if we can do this by constructing sexps instead of with quoting and unquoting.

def my_apply2(func, sequence) do
  {result, _} = Code.eval_quoted({{:., [], [func]}, [], sequence})
  result
end

This is a lot shorter. It feels kind of like working with secret knowledge though. The Elixir sexp representation is so different from ordinary written code that it’s hard to look at code that constructs code in this way and visualize what it’s building.

Here are the tests, just to prove this all works.

test "my_apply" do
  assert(my_apply(&(&1 + &2), [1,2]) == 3)
end

test "my_apply2" do
  assert(my_apply2(&(&1 + &2), [1,2]) == 3)
end

Here I’m using the new capture operator, introduced in 0.11.0, to capture an anonymous function reference to Kernel.+/2:

&(&1 + &2)

Believe it or not, this is a lot more concise than the old way. It’s still frustratingly awkward coming from Lisp, Haskell, or pretty much any other FPL. Or Ruby, for that matter, where we’d probably represent the function (method) to be applied as simply :+. In Clojure, we take a reference to the + function using, wait for it… +.

UPDATE: These tests no longer pass on Elixir 0.12.0. They generate the following error:

(CompileError) nofile: anonymous functions cannot be translated into a quoted expression, got: #Function<5.78455874 in ExercisesTest.test my_apply2/1>

I’m not sure how to fix this.

Well, now that I’ve applied myself (hah!) to this little problem, let’s see how Elixir itself implements apply. This is easy, because the Elixir API docs link directly to the relevant code on Github. Bless you for that, Mr. Valim.

Turns out, Elixir just punts it over to Erlang:

defmacro apply(fun, args) do
  quote do
    :erlang.apply(unquote(fun), unquote(args))
  end
end

I should have guessed this would be the case.

1 comment

Leave a Reply

Your email address will not be published. Required fields are marked *