Hi, dimitarvp.
I actually wanted to re-use the same struct definition in each module - not the defstruct
statement but simply the struct itself.
I ended up doing something like below to achieve my goals. This is not what I was working on but follows the same principles:
defmodule SimpleTree do
@callback order(term, term) :: integer
defstruct [:less, :value, :more]
def to_list(tree), do: to_list(tree, [])
defp to_list(tree, acc) do
acc = to_list(tree, :more, acc)
acc = [ tree.value | acc ]
acc = to_list(tree, :less, acc)
acc
end
defp to_list(tree, side, acc) when side in [:less, :more] do
case tree[side] do
%SimpleTree{} -> to_list(tree[side], acc)
nil -> acc
end
end
defmacro __using__(_) do
quote do
@behaviour SimpleTree # force user to define needed function
def init(pivot), do: %SimpleTree{value: pivot}
def add(tree, value) do
case order(value, tree.value) do
-1 -> add(tree, :less, value)
0 -> tree # value already in tree
1 -> add(tree, :more, value)
end
end
defp add(tree, side, value) when side in [:less, :more] do
Map.put(tree, side,
case tree[side] do
%SimpleTree{} -> add(tree[side], value)
nil -> %SimpleTree{value: value}
end)
end
defdelegate to_list(tree), to: SimpleTree
end # quote
end # defmacro
end # defmodule
defmodule DescendingSimpleTree do
use SimpleTree
# define order to be descending
def order(x, y) do
cond do
x > y -> -1
x < y -> 1
true -> 0
end
end
def example do
init(42)
|> add(17)
|> add(53)
|> add(52)
|> to_list
end
end
The data structure itself (SimpleTree) is given, but operations on the actual values can still be generic and overridden. One can define only the parts that change in another module - here the order of elements. How the tree is built doesn’t change, but what kinds of values are in it and how they are ordered can be defined by the user.
In my own project I aimed to re-use the equivalent of %SimpleTree{}
because the complex data structure will remain the same while the types and operations on the leaves needed to be flexible. The problem I encountered was that when putting defmodule
in a quote
block that the struct names expanded to include the names of the enveloping module. (NobbZ’ advice about prefixing Elixir.
solves that particular issue.) But in the end what I wanted to achieve was easier done by a construct like I shared above - it just took some re-thinking my approach.
For the sake of example I implemented two different kinds of operation:
-
add/2
: Needs a function order/2
defined in the module where use SimpleTree
is done. It must be defined inside the __using__/1
macro.
-
to_list/1
: Depends solely on the structure of the data itself. It can be effectively defined outside the __using__/1
macro. (So I actually pull it in with defdelegate
instead of defining it inside the macro.)
Both re-use the %SimpleTree{}
struct - it is only defined once. No multiple definitions of the same struct even if used in different modules. Module SimpleTree
knows nothing much about the data inside the value field - except that it is not another %SimpleTree{}
struct (because that would break the algorithms). All the specifics regarding the actual values inside the tree are externalized to the one invoking use SimpleTree
. This makes it a similar pattern to C++ generic programming.
(I didn’t compile my example, it was just the best I could think of when trying to show what I was trying to actually do.)