Ways to implement one data structure with three different sets of behavior

I have a situation where a “line” always contains the same data:

  import TypedStruct    # This is just a wrapper around `defstruct`. Self-explanatory I hope. 
                                     # If not: https://hexdocs.pm/typedstruct/readme.html

  typedstruct enforce: true do
    field :index, non_neg_integer
    field :coordinate, Coordinate.t
    field :action, atom
    field :string, String.t
    ...
  end

Even though the data is always the same, there are three different “types” of lines. The guiding_coordinate_for(Data.t) function depends on the type.

I could use defprotocol to produce the polymorphism, but wouldn’t that mean I’d have to give each of ContainerLine, GottenLine, and UpdatedLine its own copy of the struct? Or, less kludgy but still kludgy:

defmodule Adjustable.ContainerLine do
   defstruct [:data] # points to a Adjustable.Line)
end
defstruct Adjustable.GottenLine do 
   defstruct [:data] # points to a Adjustable.Line)
end
...

What I’m currently doing is rolling my own polymorphism. I’ve added a type field that has a value like Adjustable.GottenLine. Then Line.function dispatches on the type using apply:

  defp dispatch(name, data), do: apply(data.type, name, [data])

  def guiding_coordinate_for(data), do: dispatch(:guiding_coordinate_for, data)
  ...

Is there a better way?

Darn it, sometimes – not often – you just want inheritance. Or maybe something like Common Lisp programmable dispatching. Maybe I’ve overlooked the equivalent in Elixir.

Not sure I fully grasp the model here, but what are the friction points of your current implementation? If the only concern is that a few functions need to behave differently on the basis of a “type” field, simple pattern matching on the content of that field seems like it’d do the job…

Yes, you’re right. I turned to pattern matching as the problem got more complicated. (See below.)

I was mainly wondering how tightly bound defprotocol is to module names (/ structs) – whether I’d missed some loophole I could exploit.


The current code looks like this:

  typedstruct enforce: true do
    field :type, atom
    field :action, atom
    ...
  end

  def describe_adjustment(%{type: ContainerLine, action: :continue_deeper} = line) do
    [align_with_substring: Coordinate.un_nest(line.coordinate)]
  end

  def describe_adjustment(%{type: ContainerLine, action: :turn_deeper} = line) do
    [copy: Coordinate.previous(line.coordinate)]
  end

  def describe_adjustment(%{type: GottenLine, action: :begin_retreat} = line) do
    [center_under: Coordinate.reverse_direction(line.coordinate)]
  end

  def describe_adjustment(%{type: GottenLine, action: :continue_retreat}) do
    :erase
  end