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”?
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.
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).
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).
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
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