When to use structs and behaviours in Elixir?

I think I’m again trying to force the concept from other languages in Elixir and making my life harder. When do you resort to creating structs and behaviors? I think I may be overusing them and trying to capture their types. The end solution is becoming very hard to write and it will also not have the same guarantees of a static type.

1 Like

Can you maybe share some code that is giving you heck? I think it can be pretty hard to give a blanket answer here. I use structs when I’m modelling something concrete, but I sure don’t use them for options (there are alternatives, see NimbleOptions) and otherwise, for a lot of smaller pieces of structured data I use tuples. As for behaviours, I never really seek out using them, it usually just becomes clear when one is needed. For example, if I find myself doing dynamic dispatch then I create a behaviour.

But ya, I’m not too sure any of that is useful, I was kind of just guessing based on how I’ve seen other people overuse these things :slight_smile:

4 Likes

99% of the time I’m making a behavior it’s so that I can use Mox. Which isn’t a slight against mox, I think mox is accurately identifying that it’s time to use a behavior.

2 Likes

Ha, yes honestly I have the mostly the same experience :sweat_smile: The dynamic dispatch example happened to me for the first time in a while last week. And actually, I’m thinking we should be using it more as we have two entities that do a lot of the same things, but I’m gonna let things play out a little more before pulling that trigger.

Indeed as @sodapopcan says, the need for using behaviours (or protocols) becomes self-evident at one point. Don’t let FOMO ruin you. :wink:

It’s also best to share what problem are you solving. If my long years on this forum are any indication, you’ll get a pragmatic and nuanced advice.

2 Likes

I’d make this a bit less specific. I add behaviours once I need more than a single implementation of some abstract feature. Needing a separate test implementation (Mox based or not) is a subset of that.

4 Likes

For both, structs and behaviors I use them when I want to solidify something in the code base.
Starting with structs, I use them when I need to have a solid data shape. Classical example Ecto.Schemas, database is an external system that you want an exact and hardened mapping between the table and the application, that happens because in the common scenario you control the app code and the database migrations.
On the other hand, for me personally, entrypoints like http api(that your app is serving), message brokers or http clients(that your app is consuming) for example are stuff that you don’t have a 100% control of the data reaching your system, i’d prefer to go “processing/validating” it as it goes deeper in the app layers.

For behaviours I think of when I want a rigid contract and a way to properly communicate that. So whoever is dealing with that contract has a clear understanding of how to use/implement it. The common case is what people talked about Mox, you want a rigid contract so the actual implementation and the test mock follow the exact same contract. I have the perception that behaviors are more broadly used on libraries that want to provide some extension/dependency injection on their functionalities.

In simple terms, structs solidify data, behaviors solidify function/contracts

1 Like

And just adding that this idea of a rigid data/contract is not 100% backed by the VM. They are way more a communication tool to other developers then a guarantee from the VM.
This is not something against using them, just an acknowledgment of how they work and their limitations.
There is no way to ensure 100% that a behavior was implemented properly, or something with %Struct{} shape has the data inside it the way you expect. The elixir compiler tries to protect you from shooting yourself on the foot on most common problems, but a running system is a living thing.

I wish Elixir, or maybe Erlang’s Dialyzer, could enforce interfaces. You should be able to declare a function that takes a module that must implement some interface. Then when you call that function, passing a module that doesn’t implement that interface, you get an error.

One can dream.

3 Likes

Whoa, you’re still here @cjbottaro! :wave:

1 Like

Yes! Still writing Elixir everyday and still my favorite language. And we just posted a job listing too! :smirk:

2 Likes