Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I can see the logic behind the statement 'message passing is a pretty effective antidote to coupling' but I personally subscribe to the idea that actors don't compose. In light of that I think use of actors and messages implies some level of coupling which may not be obvious. Actors can't be arbitrarily composed together without knowing which actors they will be interacting with, making their addresses available and understanding their protocols so they can communicate together. Functions have no such requirements so I don't totally understand how message passing implies less coupling than conventional function-based architectures


I think you're assuming side-effect free functions (and apparently ones whose interfaces are automatically known, since the interface to a function is just like the message handling in an actor; in fact, Smalltalk, the original OO language, didn't have 'methods', it had 'message passing'). But, you can still write libs and share them in Elixir, to benefit from any argument you'd care to make re: functions.

Actors are all about managing state. It's when state starts leaking around or being modified unexpectedly ('wait, why is that being called there?!') that a more typical language (Java, say) starts to get tightly coupled, and sharing that state is the norm, and expected. And I agree, if you have pure functions, that issue is largely controlled for too. Just, that's a really hard paradigm to stay inside of and still be productive.

I think that's why microservices are even a thing; it forces you to decouple state from different 'functional' parts of your system; by creating separate systems you have to be very intentional about what state you share, and how you share it (database or redis or an API call or similar depending on need).

Actors do that automatically for you; the state is contained within them; there's no chance(1) it will accidentally be changed by some other process/code, and the 'controlling' process gets to decide what messages affect changes, and how those changes occur. Or, in other words, all state changes relating to a context happen in one place, rather than state being passed around to be changed anywhere willy nilly across contexts.

1 Technically there are system level things that allow you to munge state inside of an actor. This is more frowned upon than using reflection for arbitrary reasons in Java, however, so the only reason you'll ever do it is because you're debugging what's going on in something, or you have decided you absolutely -hate- your toes and need to footgun them off.


You can certainly achieve the same low coupling with anything. But ....

Messages tend to be lightweight. Often a symbol/atom "command", and a few scalar values. Like command line arguments. If the process needs more data, it sends its own message to the owner of said data.

With conventional functions and OO, you're often dealing with complex objects (with references to references ...) which often leads to breaking the Law of Demeter.

Message passing also gets your asynchronicity. You're also decoupled from specific runtime implementation (the receiving can be on a different machine, the semantics are the same). You can achieve the same without message passing, but it's certainly more baked-into the runtime/tooling/language.

Joe Armstrong's popular quote on this:

"The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle"


You can use functions without knowing their inputs and outputs, and your functions don't need environments that somehow allow them to lookup the other functions they're going to call?

Messages and function calls aren't quite isomorphic, but they're pretty close. (They get really close if you include coroutines and coroutine-like things, to cover the non-1-to-1 relationship between messages and replies that messages can have.) I'm not sure the difference can sustain the implication you're trying to draw. Especially if you include RPC in the function world.


Message passing is less coupled than functions or "objects" that share bits of state.


I think the pain-point being designated by the OP is that the cure is worse than the disease:

    Problem = tight coupling
    Solution = x, where x => {indirection, implicit_coupling}
I'm not sure I agree the final judgement call, but the analysis is spot-on. Message-passing interfaces are generally difficult to discover and to debug, and require a lot of careful documentation, logging & instrumentation.


> Message-passing interfaces are generally difficult to discover and to debug, and require a lot of careful documentation, logging & instrumentation.

You just use the "most common structures". For example, Elixir has the Agent module, which encapsulates state, but lets whatever you're using interact with the state via lambdas (or passed functions).

Another common structure is the state machine. I was somewhat displeased with the complexity of gen_statem, so I wrote my own! It's about 50 lines of code, and can have plugins to do facilities like logging and debugging via a plugin-able interface.

Ultimately the real win with message-passing is not having to write spinlocks, semaphores, or mutexes.


So you create a function around the message. It's not uncommon at all to have something like (pseudocode)

myActorModule.increment(ref, amount) -> ref ! {increment, amount}

myActorModule.syncIncrement(ref, amount) -> ref ! {increment, amount, self()} receive -> ok timeout 1000.

You can build out the message passing interface to an actor -as- functions, and even supply sync ones if you want. The actor implementation can be completely opaque to the caller; just the exported interface functions are defined, exported, and supported.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: