Not heard of it referred to as the ‘Expression Problem’, but that is a good example as to why both pure functional and OOP are very bad at solving it, protocols are not really better than OOP here, what would work best is multiple polymorphic dispatch (rehashing a bit of the article to set up context here), like CLOS (Lisps’s OOP system) supports, where you can dynamically dispatch on more than one argument, which is easily solved via pattern matching in Elixir (but requires horrible dispatch trees and such for OOP). Here’s an example (typed in post so probably errors somewhere) using my ProtocolEx library (which builds matchers at compile-time, think of it as a significantly more powerful version of the built-in Protocols):
import ProtocolEx
defprotocol_ex Overlap, as: shapes do
def overlap?(shape, other_shape), do: overlap?({shape, other_shape})
def overlap?(shapes)
end
defimpl_ex SquareRect, {%schemal{}, %schemar{}} when schemal in [Square, Rect] and schemar in [Square, Rect], for: Overlaps do
def overlap?({%Square{}, %Square{}}), do: :test_if_squares_overlap
def overlap?({%Rect{}, %Rect{}}), do: :test_if_rects_overlap
def overlap?({%Rect{}, %Square{}}), do: :test_if_rect_overlaps_square
def overlap?({%Square{}, %Rect{}}), do: :test_if_square_overlaps_rect
end
defimpl_ex CircleRect, {%schemal{}, %schemar{}} when schemal in [Circle, Rect] and schemar in [Circle, Rect], for: Overlaps do
def overlap?({%Circle{}, %Circle{}}), do: :test_if_circles_overlap
def overlap?({%Rect{}, %Circle{}}), do: :test_if_rect_overlaps_circle
def overlap?({%Circle{}, %Rect{}}), do: :test_if_circle_overlaps_rect
end
Or whatever, there’s many ways of doing it, and you can split each case of overlap into it’s own module, or put them all together, or define defaults, or control ordering, or let dependencies of this add in their own things for their own types, or use tagged tuples instead of structs, or whatever. 