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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With