"Softer" defoverideable / use at bottom of script from top

Hi, I’m relatively new to Elixir, so I apologize if my question falls under the category of “you should not want this behavior”, but I’m currently stuck on a problem that I don’t have the specific vocab to easily google.

Basically, I’m trying to import another module such that the the implementations in the imported module have lower priority than those in the module that contains the import.as a bare example:

defmodule GenericImpl do
    fn test(_) do
        :A
    end
    fn call_test(x) do
        test(x)
    end
end
defmodule SpecificImpl do
    fn test(3) do
        :B
    end
end

I’m trying to find a way for SpecificImpl.call_test(3) to return :B, but for all other inputs to return ;A.

So far, I’ve tried import (which doesn’t make SpecificImpl.call_test exist at all), using “use” to insert the source of GenericImpl directly into SpecificImpl (which makes it always return :A), using defoverrideable (which makes anything SpecificImpl.call_test(3) an error), and using “use” at the bottom of SpecificImp to insert the source from GenericImpl below SpecificImpl. This last one works, but it feels dirty, and I’d like to keep cross-module dependencies at the top of the module if possible.

Is there a proper way to do this?

1 Like

I believe the answer might be indeed “you probably should not want this”.
Instead, we usually go for one of these two more explicit approaches:

  1. We fall back to code in another module by referencing it directly:
defmodule SimpleLogger do
  def log(term) do
    IO.inspect(term)
  end
end

defmodule IntLogger do
  def log(integer) when is_integer(integer) do
    IO.puts("We have an integer:")
    IO.inspect(integer)
  end
  def log(other) do
    SimpleLogger.log(other)
  end
end
  1. If the code you want to fall back to is injected rather than in a separate module, we fall back using defoverridable.
defmodule ExampleServer do
  use GenServer
  
  # ... other callback implementations here

  @impl true
  def terminate(reason, state) when is_integer(state) and state > 42 do
    raise "You should have terminated me sooner!"
  end
  def terminate(reason, state) do
    super(reason, state)
  end
end

Both of these make it (reasonably) clear to the reader of the code what is going on, There is no ‘hidden’ logic.


That said, it is possible to directly inject code to the bottom of a module by using a before_compile-callback (See the module-documentation of the Module module no pun intended, I swear!), regardless of where use is used.
Can you give us a more concrete explanation of what you are trying to accomplish with this pattern?

1 Like

Your code looks a lot like OOP and inheritance. This is not how things are usually done in elixir. Based on your description I’d go for a behaviour based solution like this:

defmodule ImplBehaviour do
  @callback test(term) :: atom
  
  def call_test(impl, term) do
    impl.test(term)
  end
end

defmodule Generic do
  @behaviour ImplBehaviour

  @impl true
  fn test(_) do
    :A
  end
end

defmodule Specific do
  @behaviour ImplBehaviour

  @impl true
  fn test(3) do
    :B
  end
  
  fn test(term) do
    Generic.test(term)
  end
end

There’s no need for any hierarchy, meta programming or code sharing. Specific can call Generic.test/1 and it’s clear to anybody reading the code how/that this is happening.

3 Likes

@Qqwy, the concrete problem I was trying to solve is that I’ve got a handful of GenServer squid that are handling RPC calls via external message brokers. The nitty gritty of the networking is identical in all of them, and they share a number of handle_call()s, but there are a handful of unique ones per application. The goal was to have the client API and those specific handles be implemented in their own modules, and then add those handles on top of the shared ones implemented in the parent module with all the networking fuss.

1 Like

In that case you could define a custom behaviour with for example a (bad named) @callback handle_special_call(message, from, state)

Then include the handle_call implementation through a macro or use MyBehaviour (which is a macro too).

And in the quoted code returned by your macro you would have something like this:

  ... previous shared handle_info clauses

  def handle_info(msg, from, state) do
    handle_special_call(msg, from, state)
  end

  def handle_special_call(_msg, _from, _state) do
    raise "Hey, you forgot to implement handle_special_call/3"
  end

  defoverridable handle_special_call: 3

Or, as others said, you just implement handle_info/3 normally, and call the parent implementation in your last clause. It’s simpler, it works well, and does not need macros.

2 Likes

This sounds like you’d want a higher level behaviour than GenServer. Implement that for all your calls and create one implementation of GenServer which maps genserver callbacks to those higher level callbacks of your RPC calls. Your usecase might look like “composition of handle_call’s”, but it actually isn’t, you’re just operating on a to low level primitive.

1 Like

You could get this effect with defoverridable and a little boilerplate:

defmodule Foo do
  defmacro __using__(_opts) do
    quote do
      def bar(arg) do
        IO.inspect(arg, label: "default bar")
      end

      defoverridable [bar: 1]
    end
  end
end

defmodule UsedFoo do
  use Foo

  def bar("wat") do
    IO.puts("CUSTOM")
  end

  # boilerplate
  def bar(x), do: super(x)
end
2 Likes