I am trying to provide a way to create something like inheritance.
i want to be able to call a a function on a Module that inherits the function from another Module. Is the following the most straightforward way?
defprotocol Proto do
@spec foo(t()) :: charlist()
def foo(item)
end
defmodule SpecializedProto do
@callback info() :: charlist()
end
defimpl Proto, for: Atom do
@impl true
def foo(item) do
apply(item,:foo,[item])
end
end
defimpl Proto, for: SpecializedProto do
@impl true
def foo(item) do
apply(item,:info,[])
end
end
defmodule Test do
@behaviour SpecializedProto
def info(), do: "test"
defdelegate foo(x), to: Proto.SpecializedProto
end
Proto.foo(Test)
I believe you should start with describing the problem you are trying to solve, so we can give you ideas and alternative approaches. Inheritance is just one way to do things. There are others as well.
hm difficult to break down, but the library I am working on is trying to provide abstractions for embedded UI. Combinations of button presses tigger actions and combinations of leds. so i am trying to create a widget library that abstract different kind of behaviors.
it works ok well with only functions but then i need to keep track of the state initialization separately. So instead of composing functions i would rather compose very specialized modules (Test in my example) that implement a protocool (Proto) that specifies an additional init function, but then the modules use again to more generalized functions (SpecializedProto in my example).
This isn’t what protocols are for, and probably (I have not run it) doesn’t work like you really want.
For the above declaration to make sense, you’d need to be calling Proto.foo with a %SpecializedProto{} struct.
Protocols solve a particular problem: how to make a function handle many data types that aren’t known by the function’s original authors. A simple example from the stdlib is String.Chars, which defines to_string and is used by the kernel to_string function.
Without protocols, the only way to write String.Chars.to_string would be with many heads:
defmodule String.Chars
# non-protocol version
def to_string(v) when is_integer(v), do: ...
def to_string(v) when is_map(v), do: ...
def to_string(v) when is_list(v), do: ...
# etc
end
However, having custom formatting for user-defined structs would require adding new clauses to String.Chars.to_string which isn’t supported (or particularly practical).
Protocols let you do that kind of dispatching because defimpls don’t have to be supplied all-at-once like defs.
A general suggestion when prototyping something like this: start out by writing a straightforward implementation that does what you want and uses the components you’re creating. Copy-paste code FREELY during this initial part, but try to keep changes to copied code clearly labeled.
Once you’ve got things sketched out enough, only then start thinking about how to DRY things out / reduce boilerplate / do macro-fu / etc. It will be a lot more obvious where you need flexibility like protocols with working code.
This isn’t what protocols are for, and probably (I have not run it) doesn’t work like you really want.
it does work because Test is a atom. so it uses:
defimpl Proto, for: Atom do
so it would be in this case what is in oo speak a singleton
i realized i maybe simplified my example to much so now it want it to be possible with a struct or a Modul-Atom in the call:
defprotocol Proto do
@spec foo(t()) :: charlist()
def foo(item)
end
defprotocol SpecializedProto do
@spec info(t()) :: charlist()
def info(t)
end
defimpl Proto, for: Atom do
@impl true
def foo(item) do
apply(item,:foo,[item])
end
end
defimpl SpecializedProto, for: Atom do
@impl true
def info(item) do
apply(item,:info,[item])
end
end
defimpl Proto, for: SpecializedProto do
@impl true
def foo(item) do
SpecializedProto.info(item)
end
end
defmodule Test do
defstruct []
defdelegate info(x), to: SpecializedProto.Test
defdelegate foo(x), to: Proto.Test
end
defimpl SpecializedProto, for: Test do
def info(_), do: "test"
end
defimpl Proto, for: Test do
defdelegate foo(x), to: Proto.SpecializedProto
end
#IO.puts(Proto.foo(Test))
#IO.puts(Proto.foo(%Test{}))
what is not nice is the use of apply and defdelegate because the implementation is positioned in another module again.
Your example is more readable if some of the reuse of the name SpecializedProto is removed (here are the changed clauses):
defimpl Proto, for: UnimportantName do
@impl true
def foo(item) do
SpecializedProto.info(item)
end
end
defimpl Proto, for: Test do
defdelegate foo(x), to: Proto.UnimportantName
end
I’m still not understanding the intent of passing an atom versus a struct in these functions, but I suspect that’s because that argument never gets used.
For instance, think about the path that a call like SpecializedProto.info(Test) takes:
dispatched to the Atom implementation, calls Test.info(Test)
Test.info(Test) delegates to SpecializedProto.Test.info(Test)
the SpecializedProto implementation for Test structs then receives the atomTest instead of a struct
Perhaps something like this would be better:
defimpl SpecializedProto, for: Atom do
@impl true
def info(item) do
apply(item,:info,[struct(item)])
end
end
Then you don’t need the defdelegate info(...) on Test, and SpecializedProto.Test always gets the struct that it expects.