Hacker News new | ask | show | jobs
by brightball 3375 days ago
I tend to agree with you. Message passing and static types don't mesh unless there is some type of contract between the sender and receiver. It would be a nightmare.
2 comments

How does dynamic typing help if there is no contract between sender and receiver? Even in that case they​ must agree on the content of message.

Even if you insist in keeping the message untyped, with a static type system one could always convert (and possibly reject) messages as soon as they are received into a more precise type. That would keep the code that the compiler can't verify to the edges of the system.

Distributed systems are not like traditional programs, because there is not just one "edge of the system".

Every node becomes an "edge" in it's own right, and doesn't necessarily have global coherence with the rest of the system.

True, but if the sender wants the receiver to do something of value then it will need to meet a contract that the receiver enforces. That doesn't require a central repository of contracts, one node can diverge, but you must understand that parts of your network of services will start to fail. From that point of view it starts to look very much like the linker phase of a compilation, and that the types need to match up to the data structures being instantiated. It's just this 'linking' phase is in the programmers heads, and not particularly useful. A distributed system that can validate itself is a much more valuable concept.
Absolutely, the rubber meets the road at some point, nodes must understand/assume "contracts" about the data they are working with.

There already are static typed actor systems ( e.g. Orleans) which work well, but my point is that I believe OTP is more flexible for better or worse. Whether that flexibility is worth it to you for what you get is another matter.

Also I'm not sure how to think about binary compatibility between upgrades in such a system

> There already are static typed actor systems (e.g. Orleans)

Yep, I develop one myself. And have gone to the extent of not allowing senders to even post a message if it's of the wrong type (processes in nodes publish the types they accept to a central store). I initially went along with the 'accept anything' approach (which Akka really majors on too), but found that for the large systems I was developing that it became a real headache to deal with.

> but my point is that I believe OTP is more flexible for better or worse. Whether that flexibility is worth it to you for what you get is another matter.

Yep, fair enough, if it works for you, who am I to complain? It's not worth it for me, because I feel quite strongly that the code I write should understand the types it's working with. It feels like this super-late binding can give false positives, appear to work, when in fact it's not. That scares the shit out of me when systems get large.

I gotcha, I am not even an erlang programmer. My two main languages are C# and Haskell and in general I abhor dynamic types.

All I am trying to do here is enumerate the difficulties in trying to Type "OTP" in Erlang (its main selling point) , and am not commenting on all possible actor systems.

Think of it this way: you have a struct type with 4 attributes that you want to pass to another function.

Currently, that function declares it will match on the pattern of those 4 attributes rather than a static type. Now, you update the Node and modify the type on the sending Node to have 5 attributes.

With pattern matching on the 4, everything still works. With static types on the struct the contract is now out of sync.

I find this to be a really poor argument. Essentially you're lucky if your systems continue to work as others go off changing message formats without consideration for the code that will receive it?

On a suitably complex/large system this is a recipe for disaster. Things start to slowly rot. It is far better to maintain the old function, accepting the old struct, map it to the new struct and forward it on to the new function that accepts the new struct. Let the old one consume anything that's already queued, or being sent from other nodes that haven't yet been upgraded whilst the new one takes the new format.

I've worked with systems like that for years, and it's fine. We can have several large binaries with different release schedules, passing around a big struct with 50 fields and many nested structs, with different people making changes to different parts. And nothing breaks. New code accepts old structs, old code accepts new structs, no conversion code required.

To achieve that, we follow the design of Protocol Buffers:

1) Each field in each struct has both a name and a numeric id. Only ids are used for serialization, so field names can be changed at any time.

2) All fields are marked as optional or repeated, never required. Most code is written to handle missing fields gracefully.

3) Changing the type or id of an existing field is forbidden. (Note that changing the contents of a nested struct doesn't count as changing its type.)

4) Adding a new field is okay, as long as you use an id that was never used before. (Each struct definition has a comment indicating the next available id to use.)

5) Removing a field is okay if you've checked that no one is using it anymore.

6) As a small but intentional bonus, you can change an optional field to repeated while preserving binary compatibility.

In the end it works out. You can think of breakages that could theoretically happen, but they don't.

False. In erlang, your message passing is "at most once".

If you send a bad message, the receiver will crash or discard it and it is how it is intended to be.

Erlang embrace laws of maths and physics.

I would say there should always be a contract between the sender and receiver, whether that's using static types or otherwise. Not having a contract is a nightmare.

For example, say a satellite sends a number to the throttle control in feet/second, but the throttle control thinks its in m/s. To each of those systems, they're just passing a number and don't know any better.

Every JSON API call currently works without a contract. In theory it should have one, but in reality it doesn't unless the server (hopefully) validates. Either can change at any time without informing each other.

WSDL based APIs on the other hand have clearly defined contracts at both ends but there's more overhead involved.

It's not an explicit contract, but there must be an implicit one for stuff not to break