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.
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:
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
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?
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.
@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.
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.
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.
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