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

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.

1 Like

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.

1 Like

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.

2 Likes

I believe you may have miss interpreted my code as well as my post. I never compare behaviours to classes. I merely compare them to contracts/interfaces, a term that nearly every language uses and I am merely trying to draw from that for reasons of familiarity.

I did use an UML diagram to clarify my intentions, I hope this did not create more confusion than clarity. I don’t specifically believe UML diagrams are tied only to OOP languages, even though their origins may have come from them.

I generally see them as useful tools of communicating intent and design to other humans in a graphical way that is often easier to understand.


So if I get it correctly:

You can’t. Nor dialyzer nor the compiler are able to do this.

It is unclear to me. I understand people use documentation for this, but I did not grasp an idiomatic way to do this from the responses, probably because it is not possible to begin with.

This is also not possible. Since modules can be loaded at runtime, compilation checks are impossible. The best way is to fail loudly if the contract is not respected.

@LostKobrakai But surely, if I have my code in the IDE, could the LSP not detect this issue?

Or is it the only way to do this to have dedicated function like @gushonorato suggests?

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.

2 Likes

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
3 Likes

I really don’t know why people are fixated on using behaviours for polymorphism. Protocols. We have protocols! Use them.

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.

2 Likes

Yes, I would agree with you on that. Protocols are for structs/data. Behaviours are for dynamic implementations. It wouldn’t make much sense to implement GenServer callback functions as a protocol, because it’s not inherently a piece of data, but rather some implemented functions you’re giving it. ABS is not a physical type of brake, it’s a braking procedure that makes much more sense as a function than a struct. So I think your choice of Behaviours is the correct one, it just won’t give the assurances you’d get from a statically typed system.

1 Like

No problem at all! I have nothing against class diagrams, quite the opposite, actually. My interpretation came from the way you were modeling that inheritance using behaviors. Thanks for clarifying your intent. I appreciate it!

1 Like