You have stumbled upon on the many faces of the expression problem!
One doesn’t need to squint hard that the notion of your architecture to be truly scalable under development (what I mean here, strictly, is to guarantee that at no point in time you have added a new data type such as App.Accounts.User, but forgot to implement some method such as form and, conversely, you have added some method such as form and didn’t match some of the data types such as App.Accounts.User), is the same as the notion of adding a new function and a new type case at the same time.
I think what the colleagues who have answered with defoverridable suggestion imply is something like the idea from the post-scriptum of this post, which is “just being able to scale code base along one function / type case axis is enough for scalable code bases that are also ergonomic”.
That said, defoverridable is not a panacea. It allows you to add more types easily, but based on your question you seem to be more concerned about adding more functions. Furthermore, you venture into the dangerous waters of default implementations which, if unconstrainted, can quickly turn unusable. Ask yourself when have you last seen a useful default implementation that survives class inheritance?!
This is why, I would argue that what you actually need a hierarchy of protocols. One could say, you need traits! Now if you’re able to define your default implementaitons in terms of existing traits, these implementations shall trivially survive subtyping!
If you wonder where can you get hierarchical traits in Elixir, type_class library has got you covered!
What’s more, is that you can be principled about trait methods that you define and add properties for the traits that shall be checked at compile time.
Finally, on ergonomics that you desire.
Consider you have:
defclass A do
def f(x), do: x
end
defclass B do
def g(_x, y), do: y
end
defclass C do
extend A
extend B
def h(x, _y), do: x
end
The UX you want is
definst C, for: X do
def f(x), do: x.c
def h(x, y), do: {x.c, y}
end
Sadly, the best type_class can do is:
definst B, for: X
definst A, for: X do
def f(x), do: x.value
end
definst C, for: X do
def h(x, y), do: {x.value, y}
end
I sketched out an implementation plan for shallow dependency instance definitions here.
In general, I would strongly suggest you having the safest architecture that enforces completeness of function implementations over data types over both developer’s user experience and trying to chase the expression problem. Whatever it means for your particular problem.


















