mjadczak

mjadczak

Hi,

I’m currently working on a final project/thesis for my CompSci course at the University of Cambridge. I have used Elixir for about a year now and I have decided to port the GoogleDataflow/Apache Beam SDK/local runner to Elixir.

The reference implementations are written in Java and Python, and while I can write well-structured functional code from scratch, porting something using so many OOP features into idiomatic Elixir is a challenge (and one which I intend to treat in my thesis), especially since a lot of the domain-specific concepts are realised in this OOP universe only for now.

I am starting by porting many of the classes used into modules, and replacing inheritance with behaviours (which is already resulting in cleaner code IMO), planning to possibly refactor some parts into a more idiomatic structure at a later point. The sticking point for me is the case where instead of just static functionality overrides, the functionality is parametrised in Python/Java by using instances of objects. For example, elements may be assigned to windows of time, and while an IntervalWindow defines some functionality which conforms to that expected by a Window, it needs different parameters from, say, a GlobalWindow (which needs none).

The default answer for this may be “use protocols”, and indeed Flow uses structs for this purpose (but not protocols, as the windowing functions there do not have to be user-extensible). Indeed for many entities in my framework structs with protocols are the clear solution.

However for certain “behaviours” or strategies, apart from the parametrisation requirement, behaviours seem to be the right solution—a strategy for doing something seems to me like a behaviour, not a data structure. In many cases, any extensions will only need to implement a small subset of the available functionality, a perfect use case of behaviours along with default implementations and defoverridable. Protocols seem to me to imply much looser semantics and restrictions, along the lines of “do this thing, but I don’t care how you do it”, versus the behaviours’ idea of “give me this small custom function which I will plug into my larger system”.

Thinking about this then, my first idea was to simply represent these “parametrised behaviours” as tuples of {Module, data} where data is arbitrary and simply passed along to all of the behaviour functions. But immediately afterwards, I realised this is just Erlang’s tuple modules, one of the things explicitly solved by protocols!

What I am currently doing is indeed using protocols “under the hood”, however I am also using a behaviour to enforce a stricter contract on the using modules. In my __using__ macro, I make the implementing (using) module implement the required behaviour, and define an automatic implementation for the protocol which calls into these behaviour functions (possibly with default implementations).

This way the user is encouraged to think of the module as a behaviour, a pluggable strategy if you will, which can also get a struct of itself as a parameter to its callbacks. Of course there is nothing stopping more advanced users from just providing their own implementation of the protocol, but the goal here is to make it as easy as possible for users to plug their custom functionality into the system.

My question is: is this a code smell? Too much “magic”? Should I just explicitly use protocols and make users explicitly call into the default implementations if they want them? Again, usually there will be 5–6 functions in such a module and a user may override 1 or 2 of them.

I’d appreciate any advice or insight people may have on this.

Showing Posts 1 to 4

josevalim

josevalim

Creator of Elixir

All of that makes sense. Protocols are literally behaviours with custom dispatching rules, so your impression that they impose some rules in favor of some features is correct.

You are also correct that Flow could use protocols and the reason we didn’t use them is we are not ready to expose the Window materialization API.

That’s the only part I don’t agree with. If you are already using a protocol then you already have a behaviour. Protocols do not allow default implementations because it generating coupling between protocols and implementations. We already have the coupling at the specification level, which is the point of protocols, but moving it to the implementation level means that changing the default implementation of a protocol will affect all implementations that depend on it.

The __using__+defoverridable trick can also be really harmful for augmenting the protocol. For example, imagine the protocol has 2 functions today. In the future, you add a third function but you also provide a default implementation for it. Now implementations have no idea there is a third function! Maybe they would like to implement it but the default implementation means no warning is emitted. Situation can be even worse on libraries pre-1.0: what if you decide to rename a function? With the default implementation, they won’t even know something was renamed and developers will be unaware the contract even changed, wondering for hours why their implementation isn’t being called.

While we cannot solve the first problem (the one the developer will not get a warning if new functions are added to the protocols), we can solve the second one. Instead of:

# Inside quote
def add(a, b) do
  a + b
end

defoverridable add: 2

# Inside implementation
def add(a, b) do
  # ...
end

We should have:

@overridable true
def add(a, b) do
  a + b
end

# Inside implementation
@override true
def add(a, b) do
  # ...
end

This way if you override something that was not marked as overridable or something that does not exist, you will get a warning. It also improves code readability, because anyone reading the code will see the whole purpose of add/2 is to override a callback. Previously readers of the code could be left wondering why there is an add/2 function in the first place.

I want to send a proposal for the new overridable system after Elixir v1.4 is out. So you need to be aware of the limitations of the current system today.

mjadczak

mjadczak OP

Thanks for your reply. Just to clarify then, the @overridable/@override system is not in place yet, but will be submitted in a future proposal?

That’s the only part I don’t agree with. If you are already using a protocol then you already have a behaviour. Protocols do not allow default implementations because it generating coupling between protocols and implementations. We already have the coupling at the specification level, which is the point of protocols, but moving it to the implementation level means that changing the default implementation of a protocol will affect all implementations that depend on it.

The __using__+defoverridable trick can also be really harmful for augmenting the protocol. For example, imagine the protocol has 2 functions today. In the future, you add a third function but you also provide a default implementation for it. Now implementations have no idea there is a third function! Maybe they would like to implement it but the default implementation means no warning is emitted. Situation can be even worse on libraries pre-1.0: what if you decide to rename a function? With the default implementation, they won’t even know something was renamed and developers will be unaware the contract even changed, wondering for hours why their implementation isn’t being called.

I completely understand the rationale there, but like I said, this is a port from an OOP system where there is coupling at the implementation level, and a lot of the functionality depends on it. The idea is that it’s up to the developer of the library to make future changes to default implementations such that they preserve backwards compatibility or at least log some warnings to let developers know if something is wrong.

Further, this is the point I was making before as regards the “feeling” of protocols vs behaviours—protocols are more loosely coupled and just have to conform to the interface, while behaviours can have default implementations and feel more coupled. What I “need” are “parametrised behaviours”, callback functions tightly coupled to an implementation but which can also receive a few custom parameters. Hence my initial solution of using protocol dispatch since it’s already been worked on, tested and tried, but hiding that away somewhat from the user to discourage them from thinking of the module as a protocol implementation, since as you said, that implies much looser coupling than what is actually expected.

I see two possible other solutions:

  1. Ask the developers to explicitly opt into default implementations of certain functions, perhaps as parameters to the __using__ macro.
  2. Split up the large interfaces into many smaller protocols with one or two functions each, being able to opt into default implementations using @derive.

Number 1 seems somewhat verbose, while number 2 may be something I consider in the “refactoring” phase once the system is actually working.

I realise a lot of the friction here arises from trying to port a large system architected with OOP in mind to Elixir which uses different idioms, but I believe that it’s easier to first move it over into an Elixir codebase and only then make large structural refactorings. I could not find any other examples of large OOP codebases being ported over to something like Erlang or Elixir, usually people opt for a full rewrite from ground-up.

josevalim

josevalim

Creator of Elixir

Yes, it will submitted soon™.

Other than that, it seems you are well aware of the trade-offs involved, which was the goal of my original reply. I would only add that you can also provide default implementations for protocols, if you would like to, exactly as you do for behaviours (but they are also discouraged for exactly the same reasons).

I always find porting such systems fun, exactly because of the challenges in converting from OO to FP. :slight_smile:

OvermindDL1

OvermindDL1

Heh, porting my C++ ECS to Erlang years ago is still one of my most difficult conversions, I was never able to get the performance to anything that I’d consider acceptable (though the purity that I had to sacrifice in C++ for performance made it oh so clean ^.^). :slight_smile:

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
bradley
I really like the adapter patterns that ecto, nebulex, waffle, etc. use and would love find something similar for a key management servic...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
Alvinkariuki
How Can I Optimise Compile Time Dependencies I have been building an elixir application for about 2 years now. Many modules and files ha...
New

Other Trending Topics Top

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews