Intro
Whether you call it module extension, module inheritance, or module subclassing, it seems like it's not considered very idiomatic Elixir (ref).
Nevertheless, module extension is a natural thing for people to be curious about coming from other languages, and there's probably some special use cases where it's better than the alternative. How best to accomplish it?
Slightly Evil Code
As mentioned in the mailing list conversation referenced above, this gist demonstrates a (mostly) working hack that does the trick, but you might need small modifications for different versions of Elixir. Below I've changed the original binary_to_atom
call and added usage examples to discuss.
## extension.ex
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) ->
{ :erlang.binary_to_atom((<< ?x, ?A + i - 1 >>), :utf8), [], nil }
end
end
{ name, [], args }
end
quote do
defdelegate unquote(signatures), to: unquote(module)
defoverridable unquote(functions)
end
end
end
## Another file, demo.exs
require Extension
defmodule InheritedDemo1 do
def inherited() do
:result
end
def override() do
:original
end
end
defmodule ModuleExtension do
Extension.extends(InheritedDemo1)
def override() do
:new
end
end
InheritedDemo1.inherited() == :result # case 1: true
ModuleExtension.inherited() == :result # case 2: true
ModuleExtension.override() == :new # case 3: true
Everything here works as expected for the test-cases, but the implementation is pretty gnarly. To shed some light on how it works, the docs for defdelegate and defoverridable are good starting places.
More Idiomatic
The code below is an attempt at something more idiomatic, but as shown in the test cases at the bottom of the snippet, the semantics of what "extension" means are a little different.
defmodule InheritedDemo2 do
defmacro __using__(_) do
quote do
def inherited(), do: :result
def override(), do: :original
end
end
end
defmodule ModuleExtension do
use InheritedDemo2
def override(), do: :new
end
InheritedDemo2.inherited() # case 1: error
ModuleExtension.inherited() == :result # case 2: true
ModuleExtension.override() == :new # case 3: false
The third test case in particular is surprising, but actually ModuleExtension.override() == :original
. There's no compile warning or anything that I've just tried to override the unoverridable!
The way to fix this is def overridable
, described in the docs here, where the integer arguments you see are function arity.
defmodule InheritedDemo3 do
defmacro __using__(_) do
quote do
def inherited(), do: :result
def override(), do: :original
defoverridable [override: 0]
end
end
end
defmodule ModuleExtension do
use InheritedDemo3
def override(), do: :new
end
InheritedDemo3.inherited() # case 1: error
ModuleExtension.inherited() == :result # case 2: true
ModuleExtension.override() == :new # case 3: true
The only failing case now is the first one, which is because InheritedDemo3.inherited/0
is NOT defined in the module per se, it is only mentioned inside of the using macro. Fixing this last case is awkward, and considering precedents with ABCs, interfaces, etc in other languages, the compromise seems acceptable.