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