1. typing messages - this is basically trivial, you can either create a specific ADT for all the acceptable messages, or you can type messages in a "general" way as `(string, list[any])` or similar (provided that your language supports `any`)
2. typing of acceptable messages - that's way harder, as you need to model state in the typesystem - imagine "does this variable represent an open or closed file handle" and then updating that as the code calls `open`, `read` and `close` appropriately... Rust's borrowing is a simple example of that as well
3. regardless of (2), you need to figure out what to do with unacceptable messages - in a dynamically typed language like Erlang, I imagine those can happen anytime - but then even dynamic languages need to deal with "this process doesn't accept this message" so ...
this is the only mention that I found in Hamler's documentation about messages, so I'm guessing they're using the ADT approach mentioned in (1)
https://github.com/hamler-lang/documentation/blob/master/gui...
I think that would be a good use case for Dependent Types.
If you are a smaller organisation than, say, Google, ensuring compatibility between your systems is easier, so that's still doable.
For in-VM messaging, since all types are accessible and enforceable, it's easier to have a common contract amongst all your actors since the risk isn't the same.
One area I'm experimenting is is a library I co-wrote[0] in Haskell that uses the type system to attach a "version" tag to a type so that your code knows what version of a record you're looking at (and also so that you can upgrade/downgrade records between versions if you need that as well).
[0] https://hackage.haskell.org/package/DataVersion-0.1.0.0/docs...
It seems reasonable to reflect this practice in all state based data representations within a language, not just in an external system. Is the reason this isn't done as a first class citizen of language design because it's too much work to write migrators for every data structure change?
I think that might be one factor. And writing that migration by hand would be another source of potential errors to sneak into your program without some static analysis to ensure the streams in your program are handling the version of the data specified.
Making it easier to write migrators and have your whole program verified automatically has been working well for me.
From the fingers of Robert Virding itself: https://elixirforum.com/t/how-hard-would-it-be-to-have-a-str...
The way that the BEAM does that it is that it store the old and the current version of the module, any new calls made will call the functions exported in the new module, but any process already running will call the functions of the old module.
But how do you handle types in a system like that, where the type of a message can change at runtime? There's no guarantees that your valid types that the compiler just approved will be valid at runtime...
learnyousomeerlang has this quote about rolling upgrades:
"We're getting into one of the most complex parts of OTP, difficult to comprehend and get right, on top of being time consuming. In fact, if you can avoid the whole procedure (which will be called relup from now on) and do simple rolling upgrades by restarting VMs and booting new applications, I would recommend you do so. Relups should be one of these 'do or die' tools. Something you use when you have few more choices."
https://learnyousomeerlang.com/relups#the-hiccups-of-appups-...
Type-checking messages on the sending side is straightforward: just use a generic PID/actor type of some sort. So for example (using Inko [1] syntax here):
let child: Process!(String) = process.spawn {
...
}
child.send('hello')
Handling this on the receiving end is more tricky, as a receive could happen anywhere at any time. Imagine somewhere deep down you have a method like this: def foo {
process.receive
}
How would the compiler know what type `process.receive` is supposed to return? What if you want not just a string, but a specific list of strings?I'd say that for most languages the best approach is to use some kind of "Any" type for messages, and rely on some form of pattern matching to figure out what you're dealing with. This has the downside of not providing compile-time safety, but it might be good enough. For example, in Inko (this is a new addition not yet released) you would do something like this:
match(process.receive) {
'start' -> { start_the_thing }
'stop' -> { stop_the_thing }
else -> { oops }
}
[1]: https://inko-lang.org/