Ensuring module implements behaviour

Context

A feature of Elixir I find myself using often when defining functions is to pattern match on structs. This way I can ensure the function is being called with the data I expect, get nice autocompletion in my IDE and even get compile errors if I ever modify the names of the fields of the struct and forget to update them at the function definition.

Consider the (utterly simplified) example below:

def get_name(%Employee{name: name}) do
  name
end

Question

Sometimes a function takes a module instead of a struct. In those cases, it would be very useful to have a lightweight way of enforcing that the module implements a particular behaviour (either through pattern matching or through guards would be ideal).

An example with hypothetical syntax:

def sort(list, sorting_module) when implements_behaviour(sorting_module, SortingAlgorithm) do
  sorting_module.sort(list)
end

I can imagine there are more people like me who like to enforce invariants in their code. Hence my question: are there any Elixir idioms to ensure a module implements a behaviour? Or is this somehow not “the Elixir way”?

1 Like

There are not idiomatically. I have often thought it would be nice if the module() type could take a parameter declaring which behaviours it implements.

If you want, it is possible to write a function that will go ahead and check that for you, by 1) calling module.module_info(:behaviours) and seeing if it’s declared. 2) If you want to be even more paranoid (it’s possible someone ignored that compiler warning) you can also run function_exported? on the module to see if it really implemented the functions you hoped for.

1 Like

There is no way. Well, there may be one hacky way to do so, but it will not provide guarantee that it does it 100% (as we cannot check the behaviour of the implemented functions, only their existence).

Additionally functions like function_exported?/3 may behave weird in presence of dynamic module loading.

The “Elixir way” would be to simply try to use interface and if the programmer passed wrong value, then it is the error and the process that have done that should be punished (by death).

3 Likes

function_exported?/3 may behave weird in presence of dynamic module loading

I forgot about that – and I even was struggling with that a few days ago. Ironically it’ll work in prod, it’s just in dev you could tear your hair out if you depend on the module… Though if you do it in the order specified (module_info first) then I believe you are guaranteed to have function_exportes work.

process that have done that should be punished (by death).

This is the way :smiling_imp:

There are ways around that as well by using the Code module but then you just kind of do prod running of an app in a supposed dev environment. :smiley:

@aochagavia Check this thread where I helped one nice guy with a similar problem: Load all modules implementing a behaviour in escript

Maybe it can inspire your own solution to your problem.

Here is José’s breadcrumb to find the answer, there is a nice solution that is two lines of code. I might come by and post it later if people clamor that they don’t want to figure it for themselves

Is there a way to tell mix tasks to run in embedded mode?

Hm, if that’s an old command-line switch then I’d be really embarrassed that I haven’t checked.

Let it crash with an UndefinedFunctionError. A behaviour provides a set of callbacks that can be checked with @impl, but your sort/2 function should be fine with any module that defines an appropriate sort/1.

Example, from Enum.sort that accepts a module name in the second position:

iex(1)> Enum.sort([1,2,3], Application)
** (UndefinedFunctionError) function Application.compare/2 is undefined or private
    (elixir 1.13.4) Application.compare(1, 2)
    (elixir 1.13.4) lib/enum.ex:3019: anonymous fn/3 in Enum.to_sort_fun/1
    (stdlib 4.0.1) lists.erl:1022: :lists.sort/2
2 Likes

Can we pattern-match on behavior? For example,

def compute_sum(list) when is_list(list) do
  list
  |> Enum.reduce(0, fn item, acc ->
    maybe_add(acc, item)
  )
end

defp maybe_add(Computable = item, acc) do
   acc + Computable.value_of(item)
end
defp maybe_add(_, acc) do
   acc
end

or even simpler with case:

def compute_sum(list) when is_list(list) do
  list
  |> Enum.reduce(0, fn item, acc ->
    case item do
      Computable -> acc + Computable.value_of(item)
      _ -> acc
    end
  )
end

Can something of that sort be done?

No, there is no way to do that

This can be done with Protocols:

list
|> Enum.reduce(fn x, acc ->
     case Protocol.impl_for(x) do
       nil -> acc
       module -> acc + module.value_of(item)
     end)