Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

Howto dynamically generate function clauses

I need to dynamically generate function clauses, basing on the user’s config. For the sake of clarity, imagine I have a list of atoms:

@atoms ~w|foo bar baz|a

Coming from, say, config.exs. What I need is to generate this function (the MCVE is oversimplified, but it gives an impression on what I actually need):

@checker fn
  {:foo, _} -> false
  {:bar, _} -> false
  {:baz, _} -> false
  _ -> true
end

What I am currently doing is:

@clauses Enum.map(@atoms, fn tag ->
  {:->, [], [[{:{}, [], [tag, {:_, [], Elixir}]}], false]}
end) ++ [{:->, [], [[{:_, [], Elixir}], true]}]

defmacrop checker, do: {:fn, [], @clauses}

It works pretty fine, but I expect I am overcomplicating things, missing something simple. So, my question is:

Is there an easy way to generate the function clauses in compile time?

Upvotes: 1

Views: 150

Answers (1)

Dogbert
Dogbert

Reputation: 222118

I made it somewhat (see below for more) more readable using quote:

defmodule A do
  @atoms ~w|foo bar baz|a
  @clauses Enum.flat_map(@atoms, fn tag ->
    quote do: ({unquote(tag), _} -> false)
  end) ++ quote(do: (_ -> true))

  defmacro checker, do: {:fn, [], @clauses}
end

defmodule B do
  require A
  f = A.checker
  IO.inspect f.({:foo, :ok})
  IO.inspect f.({:bar, :ok})
  IO.inspect f.({:baz, :ok})
  IO.inspect f.({:quux, :ok})
end

Output:

false
false
false
true

I expected quote(do: a -> b) to work, but it's a parse error right now so we have to do quote(do: (a -> b)) which wraps the quoted fragment we want in a list.

I also expected unquote to work inside fn when it's inside quote, but that also doesn't.

iex(1)> quote do
...(1)>   fn
...(1)>     unquote()
...(1)>     _ -> true
...(1)>   end
...(1)> end
** (SyntaxError) iex:2: expected clauses to be defined with -> inside: 'fn'

I believe these two are either bugs or missing features.

Upvotes: 2

Related Questions