Sebb

Sebb

Behaviours, defoverridable and implementations - reloaded

I was just looking for documentation to build a Behaviour with some defaults.
I stumbled upon this thread:

As it seems, that’s still true in 2021. I did not find any documentation (except for the basics).

So I had a look at gen_server.ex and put a sample together (see below, see the tests for how I understand things). It does what I expect, but I have some questions:

  • am I missing some important technique?
  • (Solved with module attributes) the way I access the opts seems a little odd…!?
  • (Solved by adding @impl inside using) I get this warning I can’t figure out:

warning: module attribute @impl was not set for function foo/0 callback (specified in MyBehaviour). This either means you forgot to add the “@impl true” annotation before the definition or that you are accidentally overriding this callback
lib/user.ex:2: User (module)

my_behaviour.ex

defmodule MyBehaviour do
  @callback foobar(foo_arg :: any()) :: any()

  @callback foo() :: any()
  @callback bar(bar_arg :: any()) :: any()
  @callback baz(baz_arg1 :: any(), baz_arg2 :: any()) :: any()

  @optional_callbacks foo: 0,
                      bar: 1,
                      baz: 2

  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      @behaviour MyBehaviour
      @foo_opt Keyword.get(opts, :foo, :no_foo_opt)
      @bar_opt Keyword.get(opts, :bar, :no_bar_opt)
      @baz_opt Keyword.get(opts, :baz, :no_baz_opt)

      @impl true
      def foo() do
        {:foo, @foo_opt}
      end

      @impl true
      def bar(bar_arg) when is_atom(bar_arg) do
        {:bar, bar_arg, @bar_opt}
      end

      @impl true
      def baz(baz_arg1, baz_arg2) do
        {:baz, baz_arg1, baz_arg2, @baz_opt}
      end

      defoverridable baz: 2, foo: 0
    end
  end

  def behaviour_fun(behaviour_fun_arg) do
    {:behaviour_fun, behaviour_fun_arg}
  end
end

user.ex

defmodule User do
  use MyBehaviour, bar: :opt_for_bar, foo: :opt_for_foo

  @impl true
  def bar(arg) do
    {:bar_implemented, arg}
  end

  @impl true
  def baz(arg1, arg2) do
    {:baz_implemented, arg1, arg2}
  end

  @impl true
  def foobar(arg) do
    MyBehaviour.behaviour_fun(arg)
  end
end

test.exs

defmodule Test do
  use ExUnit.Case

  test "default impl is called and option used" do
    assert {:foo, :opt_for_foo} = User.foo()
  end

  test "default impl is called when it matches" do
    assert {:bar, :some_atom, :opt_for_bar} = User.bar(:some_atom)
  end

  test "user impl is called when default doesn't match" do
    assert {:bar_implemented, 1} = User.bar(1)
  end

  test "user impl is always called with defoverrideable" do
    {:baz_implemented, 1, 2} = User.baz(1, 2)
  end

  test "can define function in behaviour" do
    {:behaviour_fun, 1} = User.foobar(1)
  end
end

Marked As Solved

Sebb

Sebb

OK, thanks all clear now!

I’m marking this as solution for future reference, but all credit to @eksperimental!

Summary of what I learned:

  1. There are 4 types of functions related to Behaviour-modules
    1.1 fun_without_default - mandatory callbacks without default implementation, they need a @callback. They have to be implemented in the User-module.
    1.2 fun_with_default - mandatory callbacks with default implementation inside __using__. They need a @callback and have to marked defoverridable. You can’t forbid to override a function, it kinda works with some interesting effects (see above) but will cause a warning. They may be implemented in the User-module, if not, the default is used.
    1.3 optional_fun - optional callback, they need a @callback and have to be marked in optional_callbacks. They may be implemented in the User-module, if not, its an error if they are called.
    1.4 behaviour_fun - functions implemented in the Behaviour-module but not inside __using__.

  2. To access options
    2.1 use quote with bind_quoted
    2.2 put the options into a module attribute inside __using__

defmodule MyBehaviour do
  @callback fun_with_default() :: any()
  @callback fun_without_default() :: any()
  @callback optional_fun() :: any()

  @optional_callbacks optional_fun: 0

  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      @behaviour MyBehaviour

      @my_opt Keyword.fetch!(opts, :my_opt)

      def fun_with_default() do
        {:fun_with_default, :default, @my_opt}
      end

      defoverridable fun_with_default: 0
    end
  end

  def behaviour_fun() do
    :behaviour_fun
  end
end
defmodule User1 do
  use MyBehaviour, my_opt: :my_opt_value

  @impl true
  def fun_without_default(), do: {:fun_without_default, :implemented}

  def fun_that_just_calls_behaviour_fun(), do: MyBehaviour.behaviour_fun()
end

defmodule User2 do
  use MyBehaviour, my_opt: :my_opt_value

  @impl true
  def fun_without_default(), do: {:fun_without_default, :implemented}

  @impl true
  def fun_with_default(), do: {:fun_with_default, :implemented}

  @impl true
  def optional_fun(), do: {:optional_fun, :implemented}
end
assert {:fun_without_default, :implemented} = User1.fun_without_default()
assert {:fun_with_default, :default, :my_opt_value} = User1.fun_with_default()
assert :behaviour_fun = User1.fun_that_just_calls_behaviour_fun()
assert_raise(UndefinedFunctionError, fn -> User1.optional_fun() end)

assert {:fun_with_default, :implemented} = User2.fun_with_default()
assert {:optional_fun, :implemented} = User2.optional_fun()

Also Liked

eksperimental

eksperimental

init is set to be overridable.
format_status and handle_continue do not have a default implementation.

I think that pattern is good, if you want to raise on a specific pattern or type being passed, but other than that is it too much magic to define some clauses inside __using__ in another module, and maybe another library, and to define other clauses in the implementation.

Just to be clear, what I am saying is that you need to set defoverridable for every callback you are defining inside __using__, not for every callback in the behaviour. Because if you do not do this, and you do not annotate them with @impl, when you do annotate your implementations with @impl you will get a warning for every callback that you defined in __impl__ with no @impl, as it happened in your original post.

eksperimental

eksperimental

For reading the options, put them in an attribute. Btw, José published an answer related to this Unquoute not working for maps even with Macro.escape - #2 by josevalim

eksperimental

eksperimental

the warning regarding the usage of @impl is because you forgot to set it inside __using__.
If you do set it in __using__ and do not set it in your implementation, the compiler will not warn, as it realizes the one inside using is generated code by a macro.

Where Next?

Popular in Questions Top

JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&query=perfume&page=2, I would like to get: ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
hariharasudhan94
Lets say i have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => "XX...
New

Other popular topics Top

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 42842 311
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35953 110
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement