How do you extend/inherit an elixir module?

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

There is a mechanism to extend the behavior of a module. It's called a protocol. You can find more information here. You can think of an Elixir protocol as being analogous to an interface in OO.

But, in this particular case, it's like swatting a fly with a sledgehammer. I mean you could probably rewrite the code to use a protocol but if you want to simply extend the parser then fork the code and make your modification. Oh and don't forget to send a PR back to the original developer because he might like to have your fix as well.

Sometimes the simplest answer is the best one. Even if this were OO code, if some developer inherited the class or something like that I'd flag that in the code review. Why? Because inheritance leads to pathological code coupling.

In general in FP (and note that I'm making a big generalization here) the way we usually extend behavior is via higher-order functions. That is, if we want different behavior we don't use polymorphism; we simply directly pass the behavior we want to a higher-order function. What do I mean when I say "pass the behavior"?. Consider I've got some validation code for example:

defmodule V do
  def is_odd?(v) do
    rem(v,2) != 0
  end
end

defmodule T do
   def is_valid_value?(v, f) do
     if f(v), do: true, else: false
   end
end

And somewhere else I'll have T.is_valid_value?(myvalue, V.is_odd?). And suddenly my customer realizes that rather than checking if the value is odd they need to check if it's greater than 100. So I would do something along these lines:

defmodule V do
  def greater_than_100?(v) do
    v > 100
  end
end

And then I would change my call to this: T.is_valid_value?(myvalue, V.greater_than_100?)

NB: I am deliberately keeping the code pretty simple to make a point. This may not be valid syntax. I haven't checked and I can't right now.

That's it. That's all. Intelligent developers can disagree but to me that's a lot more straightforward and easier to follow than inheriting behavior and overriding it.


Perhaps defdelegate would do the trick:

defmodule MyDecoder do
  def decode(%{"X" => value}), do: value

  defdelegate decode(map), to: Decoder
end

Tags:

Elixir