Module refactoring and protocols

I am trying to refactor a module to incorporate protocols while still leaving part of the base module while handling polymorphism but am having some questions/challenges…

I have a module that handles a Strategy, it has some basic methods like new, setup, update. It also has a method called optimal that should be polymorphic.

Function optimal depends on the type of strategy. Right now the module implements one particular type of Strategy, let’s call this DominantStrategy. But eventually, I want to create another called MixedStrategy. I would like to reuse the base Strategy methods new, setup, update, while specifying the individual strategy’s implementation of optimal separately.

defmodule Strategy do
  defstruct ...

  def new(args) do ... end
  def setup(%Strategy{} = s, data) do ... end
  def update(%Strategy{} = s, data) do ... end
  def optimal(%Strategy{} =s) do ... end
end

Let’s say I were to create a protocol called Decision which declares optimal

defprotocol Decision do
  def optimal() 
end

Then I would create the code implementing the protocol like so:

defmodule DominantStrategy do
  defstruct ...
  def optimal(%DominantStrategy{} = strategy) do
    ...
  end

  defimpl Decision, for: DominantStrategy do
    def optimal(%DominantStrategy{} = strategy) do
     DominantStrategy.optimal(strategy)
    end
end

defmodule MixedStrategy do
  defstruct ...
  def optimal(%MixedStrategy{} = strategy) do
    ...
  end
  defimpl Decision, for: MixedStrategy do
    def optimal(%MixedStrategy{} = strategy) do
     MixedStrategy.optimal(strategy)
    end
end

So my question is: how do I make this work with the Strategy module if the DominantStrategy and MixedStrategy structs are the same as the Strategy struct? Is there a more FP way to handle this?

Specifically:

  • Do I eliminate the Strategy struct and hence the %Strategy{} = s pattern match? 
    
  • Do I operate on the DominantStrategy struct and MixedStrategy struct generically in my top level methods like setup/update?
    
  • Does Strategy.new now return the specific Strategy implementation struct?
    
  • How to ensure the different structs (DominantStrategy, MixedStrategy) adhere to a common structure?
    

    defmodule Strategy do
    def new(args) do
    case args.type do
    :mixed -> MixedStrategy.new(args)
    :dominant -> DominantStrategy.new(args)
    _ -> raise “Error”
    end

    def setup(s, data) do ... end
    def update(s, data) do ... end
    def optimal(s) do Decision.optimal(s) end
    

    end

Apologies in advance if this question type has been previously asked. A link would be appreciated.

If not, I would appreciate suggestions on the best way to go about this

PS. The presentation Well-Typed Elixir briefly touches on this.

– Thank you
Bibek

It really sounds like you want a behaviour, not a protocol.

Protocols are for calling the same function on different data types.

Behaviours are for a custom implementation of a function for each module. In essence a behaviour is a promise that your module implements a function of name X with arity Y. So that if I pass in
the module name to another module it can use

Kernel.apply(module, :optimal, [strategy])

In the case of Structs, they are really just Maps with a special key that identifies the module that they are associated with.

iex> foo = 1…3
iex> bar = Map.get(foo, :"__struct__")
iex> Kernel.apply(bar, :“range?”, [foo]) == Range.range?(foo)

Thanks there @bbense.

I guess more than passing around different types I am more passing around modules – hence the use of behaviors.

This older post helps to distinguish between the different types of polymorphic activity

My only minimal requirement would be for another module to abstract away the custom behaviors so my client code doesn’t need to worry about DominantStrategy vs. MixedStrategy