This isn't really true.
You can define a channel where the message type is an interface, and then you can use a type switch on the receiving side to downcast that interface to various concrete message types.
What you can't do without sum types is ensure that you don't need a "catch-all" branch for if/when a value is sent down the channel that isn't one of the concrete message types you expect, but it will still be type safe and polymorphic.
What OP probably means is that there’s no way to build a channel transmitting a list of various types while having the compiler warn you if you’ve forgotten a possible type, or if you’re trying to unwrap into an impossible one.
Thanks.
> What OP probably means is [...]
I already completely addressed this point in my previous comment. Your comment doesn't appear to add anything new to this discussion. What OP actually sounded like they were saying is that you would have to do an ad-hoc union type with a struct containing the fields of the various things you would want to send, and having to hope you don't access the wrong fields at the wrong time. That would be type unsafe.
I've written a lot of Rust professionally over the years, among other languages. I'm very familiar with what strong type systems look like. Being told my standards are "very very low"... such a great way to keep this conversation constructive. Type safety is a spectrum, and what I described in my comment above is far from "very very low standards" when talking about practical applications. It's not describing the gold standard -- I'll be the first to say that I wish Go had proper sum types -- but some people apparently forget what a lot of other popular languages deal with in terms of type safety, leading to hyperbolic statements about other people's standards.
The main thing I am working on (slowly, because time and stuffs) is a client library for an saync-protocol messaging/forum platform, where each request will (eventually) receive a response, so I send the request, then construct a request-specific data type to pass across to the listener, together with the ID of the request, so that when the response returns, it can dispatch to the proper decoder for the response.
That actually seems easier to describe in code, than in words, but this margin is too short for that.
And with mutex one can build those as necessary, even if interaction with channels is ugly.
receive
{high_p, Message} ->
process_high(Message)
after 0 ->
receive
{high_p, Message} ->
process_high(Message);
{low_p, Message} ->
process_low(Message)
end
end
The Go equivalent can be done with different channels for priorities and similarly nested selects, as I am sure you are aware.With this code:
receive
{high_p, Message} ->
process_high(Message);
{low_p, Message} ->
process_low(Message)
end
if the process has one high_p message and a low_p message, the high_p message will be processed first.Erlang doesn't have priorities; you can fake them with selective receive, but IIRC selective receive is considered a code smell and something that should generally be avoided or minimized.
(You can also have a separate process act as a priority queue channel by, well, storing messages in an actual priority queue structure and sending from its head, and that's a little more robust.)
As evidence that channels should be used rarely, and only in small scopes, consider the number of places chan is used in the standard library.
It's good if you don't want to care about those details, it's bad if you really care about those details for performance reasons.
The former is a nightmare for security and productivity, the latter is a nightmare for performance.
It can decide to heap-allocate or stack-allocate objects (when you new them) based on escape analysis, which is a JIT optimization. But that's very different from heap-allocating variables.
Regardless, whether something is a local or not in Java still varies depending on the JIT's decisions. (Or rather, the JIT does not see local/non-local variables, only lifetimes, and whether the lifetime may extend past the JIT's current view.)
In Go, if you have a local of type int, and that local is captured by a closure, it will be allocated on the heap to extend its lifetime. Java doesn't need to do this because it requires captures to be effectively final. On the other hand, C# does the same thing as Go.
You don't have to think about stack vs. heap allocation in Go unless you're writing performance sensitive code. You could pretend that Go heap allocates everything and still read and write Go code without any problems.
Thankfully plenty of other GC based languages hadn't made the same mistake.
So why CSP which does not allow to implement priority delivery or multicasting and makes cancelling much harder compared with normal polymorphic message queue per thread? Moreover, Go crippled CSP even further as one cannot select on channel and file descriptor.
Fortunately Go does provide mutexes and signals so one can ignore channels unless required by Go API. Still without proper sum types polymorphic and type-safe message queues are not possible.