Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you extend/inherit an elixir module?

Tags:

elixir

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?

like image 569
anshul Avatar asked Feb 09 '16 21:02

anshul


2 Answers

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
like image 148
samvv Avatar answered Nov 17 '22 17:11

samvv


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.

like image 20
Onorio Catenacci Avatar answered Nov 17 '22 17:11

Onorio Catenacci