elixir

How do you extend/inherit an elixir module?


Let's say an elixir library defines:

defmodule Decoder do

  def decode(%{"BOOL" => true}),    do: true
  def decode(%{"BOOL" => false}),   do: false
  def decode(%{"BOOL" => "true"}),  do: true
  def decode(%{"BOOL" => "false"}), do: false
  def decode(%{"B" => value}),      do: value
  def decode(%{"S" => value}),      do: value
  def decode(%{"M" => value}),      do: value |> decode
  def decode(item = %{}) do
    item |> Enum.reduce(%{}, fn({k, v}, map) ->
      Map.put(map, k, decode(v))
    end)
  end
end

I want to define a module MyDecoder which just adds one more def decode to the above module. In an oo language, this would be done by inheritance/mixin/extends of some sort.

How do I do this in elixir?


Solution

  • Apparently, you can. Take a look at this gist which uses some rather "obscure" methods for listing a module's public functions and then generating delegates out of them. It's pretty cool.

    Here is where it's all about:

    defmodule Extension do
      defmacro extends(module) do
        module = Macro.expand(module, __CALLER__)
        functions = module.__info__(:functions)
        signatures = Enum.map functions, fn { name, arity } ->
          args = if arity == 0 do
                   []
                 else
                   Enum.map 1 .. arity, fn(i) ->
                     { binary_to_atom(<< ?x, ?A + i - 1 >>), [], nil }
                   end
                 end
          { name, [], args }
        end
        quote do
          defdelegate unquote(signatures), to: unquote(module)
          defoverridable unquote(functions)
        end
      end
    end
    

    You can use it like so:

    defmodule MyModule do
       require Extension
       Extension.extends ParentModule
       # ...
    end
    

    Unfortunately, it throws a warning on the most recent Elixir builds, but I'm sure that can be solved. Other than that, it works like a charm!

    Edited so as not to throw a warning:

    defmodule Extension do
      defmacro extends(module) do
        module = Macro.expand(module, __CALLER__)
        functions = module.__info__(:functions)
        signatures = Enum.map functions, fn { name, arity } ->
          args = if arity == 0 do
                   []
                 else
                   Enum.map 1 .. arity, fn(i) ->
                     { String.to_atom(<< ?x, ?A + i - 1 >>), [], nil }
                   end
                 end
          { name, [], args }
        end
    
        zipped = List.zip([signatures, functions])
        for sig_func <- zipped do
          quote do
            defdelegate unquote(elem(sig_func, 0)), to: unquote(module)
            defoverridable unquote([elem(sig_func, 1)])
          end
        end
      end
    end