Fl4m3Ph03n1x

Fl4m3Ph03n1x

How to make a module depend on a behaviour?

Background

I am trying to write some code to exemplify the power of behaviours in Elixir.
As a result, I am trying to implement this diagram:

Basically, I have a Car module, which depends on a behaviour (interface) Car.Brake. There are two possible implementations of this behaviour, the Car.Brake.ABS and Car.Brake.BBS respectively.

Code

The way I modeled this concept into code is as follows:

lib/brake.ex:

defmodule Car.Brake do
  @callback brake :: :ok
end

lib/brake/abs.ex

defmodule Car.Brake.Abs do
  @behaviour Car.Brake

  @impl Car.Brake
  def brake, do: IO.puts("Antilock Breaking System activated")
end

lib/brake/bbs.ex

defmodule Car.Brake.Bbs do
  @behaviour Car.Brake

  @impl Car.Brake
  def brake, do: IO.puts("Brake-by-wire activated")
end

lib/car.exs

defmodule Car do
  @type brand :: String.t()
  @type brake_system :: module()
  @type t :: %__MODULE__{
          brand: brand(),
          brake_system: brake_system()
        }

  @enforce_keys [:brand, :brake_system]
  defstruct [:brand, :brake_system]

  @spec new(brand(), brake_system()) :: t()
  def new(brand, brake) do
    %__MODULE__{
      brand: brand,
      brake_system: brake
    }
  end

  @spec brake(t()) :: :ok
  def brake(car), do: car.brake_system.brake()

  @spec change_brakes(t(), brake_system()) :: t()
  def change_brakes(car, brake), do: Map.put(car, :brake_system, brake)
end

Objective

The idea here is that I can create a Car and decide (and even change) which breaking system is used at runtime:

> c = Car.new("Honda", Car.Brake.Bbs)
%Car{
  brand: "Honda",
  breaking_system: Car.Brake.Bbs
}

c = Car.change_brakes(c, Car.Brake.Abs)

%Car{
  brand: "Honda",
  breaking_system: Car.Brake.Abs
}

Problem

So far so good. But now comes the problem. I am using dialyzer to check the types. And while the above code sample will not trigger an error in the compiler (nor in dialyzer) the following code sample will also not trigger any errors, while still being obviously wrong:

c = Car.new("honda", Car.WingsWithNoBreaks)

The issue here is that Car.WingsWithNoBrakes does not implement the Car.Brakes behaviour, but no one complains because of my lax typespec @type brake_system :: module()

Questions

With this in mind, I have several questions:

  • How can I make my typespec more specific, to verify the module being passed actually implements the behaviour?
  • If this is not possible, is there a more idiomatic way of specifying this dependency clearly?
  • What is the idiomatic way in elixir to specific that “module A” depends on an implementation of “behaviour B” ?

Marked As Solved

LostKobrakai

LostKobrakai

There’s a few steps:

x = Car.WingsWithNoBreaks
c = Car.new("honda", x)

Here there’s nothing to be done at compile time / Car.new. From the AST level all it gets is a variable x and that’s it. What x will be at runtime is unknown to the AST.

The typesystem upcoming might have more information (as in that x will always resolve to Car.WingsWithNoBreaks in this specific case), but that’s the next step:

c = Car.new("honda", Car.WingsWithNoBreaks)

Here the AST is at least aware of a specific value – an atom. That atom might or might not reference a module. It might not reference a module when this code is compiled, but might successfully do so once this code is run at runtime.

Again go the optimistic route: The atom references an existing module and it doesn’t change up until the code is actually run at runtime.

You can check of the module has functions as defined on the behaviour using function_exported?. That does check if functions of the correct name and arity are exported by the module.

It still doesn’t check that those functions can handle all the inputs they will eventually get provided, nor will it check that those functions return values expected to be returned based on the interface of the behaviour. That’s again information that cannot be gleamed from looking at the elixir AST (basically the source code in different form).

This is again a place where the new upcoming typesystem may eventually be able to check that stuff, but by now it doesn’t do so.

And all of that is assuming the callback implementations are actually correct and don’t have bugs, which happen to not violate the interface, but expectations not encodable in typespecs.

So all in all:

  • No this is not “but surely this can be done”
  • If attempted it would be full of footguns and false positives.
  • Even if you do what can feasably be done today you’re still checking just a subset of error cases, so you cannot get rid of handling error cases at runtime anyways
  • Some refactoring like extracting the module name to a variable or even another function makes this problem vastly more difficult outside of a typesystem, given how common that is it’s questionable if the effort to implement the above is worthwhile at all

Also Liked

LostKobrakai

LostKobrakai

Callback modules in elixir are just a collection of callback functions. And callback functions even in other languages can do whatever they want to do. You can expect them to work a expected way (ducktyping) and you can document that expectation (e.g. type/behaviour specs) but there‘s no way to make sure they actually do adhere to those expectations.

The fact that modules can be added/loaded or even reloaded at runtime also make any compile time checks rather impossible in a generic way.

So long story short - this cannot be caught automatically. Fail loudly at runtime if the module or a callback breaks the expected contract.

pejrich

pejrich

Ultimately the assurances of statically typed languages cannot be given in a dynamic system, because the very system itself has the potential to change. So to reiterate what others have said, there isn’t really a way to achieve what you’re wanting to do, and most of the things that might seem to help give these guarantees, will only give you false confidence.

iex(28)> ABS.__info__(:attributes)[:behaviour]
[Car.Brake]

This might look promising at first, but it only really confirms that @behaviour Car.Brake is in the file, not that break/0 is.

iex(31)> ABS.break
** (UndefinedFunctionError) function ABS.break/0 is undefined or private
    ABS.break()

As others have said, you can make calls to function_exported?/3 to get more assurances, but ultimately these are runtime checks, and the question with all runtime checks should be “And what exactly do I plan to do if it’s not what it’s supposed to be?”. If you think the error message being thrown at Car.new/2 is a more helpful place for it to be raised rather than when NotReallyABrake.brake() is eventually called, then that might be a valid reason to do a check there, but I’m always personally quite against it.

Writing exception handling code now, in the hopes that it can, at some point in the future, diagnose and cure an unknown bug that I myself couldn’t avoid ending up in, is just setting myself up for almost certain disappointment, plus making the code more complex to boot. It’s almost always a lose-lose.

In the rare occurrences where I have something like this, I have in the past just added a guard:

def new(brand, brake) when brake in [ABS, BBS, etc.]

But only if there was a really valid reason to not just let invalid modules just crash naturally. Obviously with this approach you have another touch point you need to update if a new brake type is added, which forgetting to do so could itself introduce a bug(e.g. passing in a module that does correctly implement Car.Brake, but just wasn’t added to the guard list).

I think this is one of those static typing reassurances that just can’t be attained in a dynamic language without also bringing in all the other static language baggage. Remember that even though it’s not quite common place in Elixir, it’s possible for the very behaviour itself to change at runtime! So how can a compiler give you any assurances against a system which is completely malleable after the point which it would give those assurances.

Personally I would look at Behaviours as being suped up documentation with the added benefit of compile time warnings for the implementing module. So it can clearly state to someone seeing the module what this module is for and what it can do. And for someone writing a new module for a new Car.Brake type, it can make sure they have correctly implemented the callbacks as they’re currently defined. But not that Car.new/2 will always receive a module that implements that Behaviour. As far as I’ve seen, your @type brake_system :: module() typespec is the correct one to have there.

Fl4m3Ph03n1x

Fl4m3Ph03n1x

My understanding is that behaviours are normally used for contracts (like I am trying to achieve) while Protocols are normally used for data:

In my example, ABS and BBS are not data structures. They are modules which can perform complex actions (such as breaking to slow down a car). I personally do not think Protocols would fit this example properly.

lud

lud

As far as I know this is not possible.

The workaround in a functional programming language is to pass functions and not modules. But in Elixir we tend to use modules of that sort of things, mostly because passing each callback separately is not really practical.

What is the idiomatic way in elixir to specific that “module A” depends on an implementation of “behaviour B” ?

I don’t think that it is possible either. I’d really like a Dialyzer that supports behaviours but it is not the case. This is also why I do not like the pattern with injection used with Mox where you put the adapter in runtime config and have an adapter() function that returns the currently defined module. To me this should be a compile time thing.

In your case it is the same problem. I assume Car.WingsWithNoBreaks does not exist. But Dialyzer cannot even check that a module will exist or not when the module is passed dynamically.

gushonorato

gushonorato

Behaviours in Elixir are simply sets of function specifications (callbacks) that a module must implement. They should not be confused with Object-Oriented Programming concepts. Behaviours are not classes.

If you want to check whether a module fully implements a behaviour, you can verify that it provides all the callback functions defined by that behaviour. For example:

defmodule BehaviourChecker do
  def implements?(module) do
    callbacks = Brake.behaviour_info(:callbacks) || []

    Enum.all?(callbacks, fn {fun, arity} ->
      function_exported?(module, fun, arity)
    end)
  end
end

If you need to perform these checks at compile time, keep in mind that Elixir doesn’t offer robust static typing, which makes full static verification difficult. Depending on your requirements, you can use compile-time checks with macros, but comprehensive static analysis isn’t straightforward.

Where Next?

Popular in Questions Top

pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
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
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
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

We're in Beta

About us Mission Statement