elixir

Check that module implements behaviour


I have a behaviour and a function that takes a list of modules that should implement that behaviour. I would like to check that each module passed in does in fact implement that behaviour. I can do that with MyBehaviour.implemented_by?/1 below, but I'm wondering if there's a more direct way about it.

defmodule MyBehaviour do
  @callback do_something(String.t(), String.t()) :: no_return()

  def implemented_by?(module) do
    :attributes
    |> module.module_info()
    |> Enum.member?({:behaviour, [__MODULE__]})
  end
end

Is that the best way to check that? I'm not finding anything in the docs or Elixir forum or anywhere.

Should I even be checking that? Or should I just let the responsibility rest entirely upon the caller? Are behaviours more about "I want to make sure I implement everything needed" than "I want everyone else to know I implement everything needed"?

Is there a way to use behaviours as a type in typespecs? Can my function spec say that args should implement my behaviour, or should I just use module()/atom()?


Solution

  • Interesting question.

    Are behaviours more about "I want to make sure I implement everything needed" than "I want everyone else to know I implement everything needed"?

    My understanding is that a behavior is a contract between a module author and the user of that module to say: "I expect you to provide me with a module that can do all of these things". So it is the responsibility of the module user to do that.

    The fact that the keyword for a behaviour function is @callback seems to me to say that usually the module that defines the behaviour is also that module that will be consuming that behaviour (in other words, calling the callback). It seems to be the responsibility of the behaviour implementor to make sure it correctly implements the behaviour, with a compile-time check to help them out, but there is no run-time help for the user of a module that requires the behaviour to make sure they actually provided a valid implementation.

    Your solution to provide a run-time warning looks good to me - however it's possible to implement a behaviour without providing the @behaviour attribute, so it will not work well in that case.

    There is a slightly more helpful error message if the behaviour implementor declared @behaviour in their code but ignored the compiler warning:

    warning: function foo/0 required by behaviour ExpectBehaviour is not implemented (in module ClaimsItImplementsButDoesNot)

    iex> ExpectBehaviour.use_behaviour(ClaimsItImplementsButDoesNot)
    ** (UndefinedFunctionError) function ClaimsItImplementsButDoesNot.foo/0 is undefined or private,
       but the behaviour ExpectBehaviour expects it to be present
    

    However this is not the case if you simply pass an unrelated module that doesn't implement the behaviour:

    iex> ExpectBehaviour.use_behaviour(DoesNotImplementOrClaimTo)
    ** (UndefinedFunctionError) function DoesNotImplementOrClaimTo.foo/0 is undefined or private
    

    Is there a way to use behaviours as a type in typespecs?

    A behaviour is not a type, it's a specification of a set of functions, and a module can implement multiple behaviours, so I don't think this makes sense. As mentioned above, it seems sensible to keep the use of a behaviour's callbacks constrained to the module where it's defined.