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.