What is the pattern for the same function implemented in different modules (strategy pattern)

This works, but I’d like to clean it up a bit (extra functions in caller):

defmodule whatever do
  defp do_thing(%{source: "foo"}) do
    IO.puts("calling foo")
  end

  defp do_thing(%{source: "bar"}) do
    IO.puts("calling bar")
  end

  def handle_info(:dont_care, state) do
    SomeContext.some_query
    |> Enum.each(fun record ->
      {:ok, yeah} = do_thing(record)
    end)
  end
end

But moving the extra private functions into their actual modules results in this error:
function do_thing/1 imported from both Bar and Foo, call is ambiguous

defmodule Foo do
  def do_thing(%{source: "foo"}) do
    IO.puts("calling foo")
  end
end

defmodule Bar do
  def do_thing(%{source: "bar"}) do
    IO.puts("calling bar")
  end
end

defmodule whatever do
  import Foo, only: [do_thing: 1]
  import Bar, only: [do_thing: 1]
  ....
end

I looked at protocols but didn’t see how they could clean this up. I’d like to just call a modules’ do_thing method without having a wrapper method for each one. Is there a way to do this? Thanks.

I think you can simply include the module when calling the function with Foo.do_thing(), and that importing or aliasing a module is “just” a way to avoid having to use the module name for every call.

Edit: see here and here for fuller and more accurate information.

1 Like

Hi @BrightEyesDavid,

Explicitly calling Foo. is what I am trying to avoid.

To call Foo. I’d have to match on source in a case statement, or pattern match in the do_thing function args (1 for each source type).

I am doing the latter above, but am wondering if there is a way to just call either modules function directly based on the pattern in the struct.

To summarize, it looks like two functions can have the same arity and arg types

  • if they match on a different value in the struct arg
  • and are defined in the same module

but

  • that doesn’t work if the function are defined in different modules and then imported

Edit: to give an example of what I have now, and it’s ok and works, but might be improved is this:

defp do_thing(%{source: "foo"} = record), do: Foo.do_thing(record)

Ah, sorry. :slight_smile: I did wonder if I was missing something.

Hi, @rschooley! That’s a pretty abstract question/sample hehe

But, if your do_thing is a call that make a thing so different with each kind of record, I would guess that it structure changes a lot depending on its source. If is that the case, I think it make sense that each type of record is a struct and that exist a ThingDoer protocol, so you could easily extend this thing to be done to other record types in the future. It would be something like that:

defmodule FooRecord do
  defstruct [:content, source: "foo"]
end

defmodule BarRecord do
  defstruct [:content, source: "bar"]
end

defprotocol ThingDoer do
  def do_thing(record)
end

defimpl ThingDoer, for: FooRecord do
  def do_thing(record), do: IO.puts("calling foo")
end

defimpl ThingDoer, for: BarRecord do
  def do_thing(record), do: IO.puts("calling bar")
end

defmodule ThingDoerBoss do
  def handle_info(:get_thing_doers_to_work, state) do
    [
      %FooRecord{content: "lol"},
      %BarRecord{content: "xpto"},
    ]
    |> Enum.each(&ThingDoer.do_thing/1)
  end
end

With that solution you can easily extend the do_thing to other source structures and also have the record structures documented.

PS.: In that case, your SomeContext.some_query must return the records as structs.

PS2.: I would not recommend you to use this import overriding thing even if it worked, it seems that would be a hell of situation to navigate through modules trying to discover in which of them are the matching function hehe

2 Likes

To clarify things a bit. In module Whatever you are only defining one function do_thing/1 which has two clauses. While it looks 2 separate functions they are actually parts of the same function. In the modules Foo and Bar you are defining 2 different functions do_thing/1 so now you have 3 functions do_thing/1 in separate modules. What uniquely defines a function is the module its in, its name, and its arity (number of arguments). Importing does not change that, it just gives you a way of not having to prefix calling a function in another module with the module name. You still have 3 different do_thing/1 in 3 modules.

3 Likes

Interesting… thanks for this!