Elixir-Code-Smells
Elixir-Code-Smells copied to clipboard
Suggestion: Mixing up behaviours and protocols
I'd like to see something about situations where you would prefer to use protocols over behaviors, or vice-versa. This is a hard decision to make, but I think I've developed a preference for protocols.
In general, I've seen behaviours misused more than protocols, where a behaviour will expect a struct of the using module's type, basically re-creating protocols but without some compiler niceness. Passing around a module name and then calling a function on it always feels awkward. I'd rather call a function on a named module so I can make sure the function exists at compile-time. I think it's harder to misuse protocols, especially because I do think they would still work in a lot of situations where behaviours would.
Here are a few things I read about this subject which can make clear that it's a hard choice:
- https://stackoverflow.com/questions/33704618/why-is-elixirs-access-behaviour-not-a-protocol
- https://web.archive.org/web/20180828125323/http://manuelalb.com/elixir/polymorphism/2017/02/14/behaviours_vs_protocols.html
- https://web.archive.org/web/20181127095939/https://www.djm.org.uk/posts/elixir-behaviours-vs-protocols-what-is-the-difference/
I would welcome a discussion; like I said this is a very nuanced decision where I think some advice would be very useful.
I'd rather call a function on a named module so I can make sure the function exists at compile-time.
Won't we still get a runtime error ** (Protocol.UndefinedError) protocol <ProtocolName> not implemented for ...?
Behaviours are simpler and, somehow, easier to reason about (though, I might be just not experienced enough with protocols).
However, a place where protocols fit better I think is when working on a library that we want to provide with the ability to extend the functionality (cos an implementation for a protocol from a library can be defined in the application code)
Won't we still get a runtime error
** (Protocol.UndefinedError) protocol <ProtocolName> not implemented for ...?
That will still happen, yeah. I just mean in the context of calling MyModule.myfunction/1, I can get a compile-time assurance that myfunction/1 exists on MyModule, but behaviours don't provide that, since you're calling apply(module, :myfunction []). There are guarantees that modules using behaviours have all the behaviour's functions are implemented, and they're similar to protocols there. So the caller of a protocol gets an additional check that behaviours don't.
Behaviours are simpler and, somehow, easier to reason about (though, I might be just not experienced enough with protocols).
I agree with you there. And I think that's why folks seem to lean towards behaviours. Protocols kind of twisted my brain, at first, but I've developed an appreciation for them and I have a habit of preferring them over behaviours now.
However, a place where protocols fit better I think is when working on a library that we want to provide with the ability to extend the functionality (cos an implementation for a protocol from a library can be defined in the application code)
That's where the question is, in my opinion. Both behaviours and protocols define a set of functions for the next developer to implement, so it's possible to use either. I think protocols are a little more flexible, because you can implement a protocol for someone else's struct, but you can't force someone else's module to implement a behaviour.
However there's the question of who makes the struct or where it comes from, which is a question that behaviours don't need to answer. To answer that question, a consumer of a protocol could take a configured module name, try to instantiate a struct from it, then pass it into a protocol. That gets them the same kind of dispatch that behaviours do. That also feels unpleasant to me, because structs should be data types, not just flags to dispatch a protocol on, but I hesitate to call it a smell because I can't think of any problems it would cause.