5 ms·
Oh ok that makes sense. Either has a little more to it, as I am sure you are aware, specifically Type Disjunction, but a crappy version of Result is all anyone
by benjaminjackman 12y ago
Oh ok that makes sense. Either has a little more to it, as I am sure you are aware, specifically Type Disjunction, but a crappy version of Result is all anyone ever really uses it for as far as I have seen.
Real quick for those that don't understand, when I say Type Disjunctions (aka union types) I mean a type which has a value which the type system is guaranteeing is 1 of X different types. So Either[String,Int] is a specialized case where X=2 and the value is either a String or an Int. This ends up being really similar to Tuples, Tuples are often given their own syntax, typically, (A,B,...) and frequently in languages that don't support the tuple abstraction natively, things like std::pair<A,B> pop up as a poor substitute special cased to X=2 element tuples.
This leads to my view that Either is to Type Disjunctions as something like std::pair is to Tuple, scratching the surface of a deeper and much richer abstraction. When it pops up in your language its a symptom of lacking type unions, just as std::pair is a symptom of lacking tuples.
Do you think Rust will ever have native support for type disjunctions?
I understand that it probably principly uses object hierarchys for this type of stuff, and in a lot of ways that makes a lot of sense, but often time when writing statically typed message passing code, aka Consumers / Producers, the more ad hoc representation + syntax support for unions eliminates huge traunches of boilerplate and provides static guarantees that all the cases are handled consider this code for example.
As real world, recurring pain in the neck example, in Scala to preserve type safety and exhaustiveness checking at my company we create a sealed trait hierarchy where each subclass wraps each different type in the disjunction.
It's a poor man's type disjunction and it takes A LOT of boilerplate.
Its still worth it to jump through these hoops because in a message passing system built around producers/consumers(aka, sources/sinks, emitters/handlers etc) we get the following important benefits:
a. Only messages that fall in a set of allowed types can be passed into a handler, guaranteeing that a message the handler can't handle won't be passed to it.
b. A handler is typically backed by a match expression which handles the different subtypes the handler handles. We used a sealed trait in scala pattern which guarantees that if someone adds another type to the hierarchy without adding it to all the handlers (which might be in different codebases) there is a compile error for a non-exhaustive match in the handler's implementation instead of a runtime blowup there when the unhandled type falls through the match.
Obviously this is just scratching the surface of the benefits that type disjunction can provide. Hopefully enough programmers and language designers will have an 'aha moment' and realize that just as adding lambda's to abbreviate anonymous functions opens up the world of higher order functions, and tuples just eliminate doing the same thing thousands of crappy different ways, and having an Option (aka Maybe) type eliminates null pointer exceptions, and hopefully standard library sanctioned Result type eliminates unhandled exceptions, having language support for type disjunctions will have benefits similar to all these other `essential` features.
TL;DR Type Disjunctions are really useful, and are missing fundamental in most type systems do you think Rust will support them?
- comex 12y agoRust has always supported type disjunctions :) Result is simply defined as pub enum Result<T, E> { Ok(T), Err(E), } where enum is a general 'variant type' mechanism. (Nor is Rust a fan of type hierarchies in general - it doesn't even have subclassing.)
- benjaminjackman 12y agoOh well I guess I will have to read up on rust some more then before writing long winded posts :) That's very cool. Does it allow inline type disjunction declarations? Rather than (never written rust before) something like: pub enum ParseResult { ParseError(Err) IntResult(Int) StringResult(String) } def parse : ParseResult Allowing something like this newtype ParseError = Err def parse : ParseError | Int | String = ... This avoids having to create specific names for each different type in the disjunction. I mean we could implement tuples like this pub tuple MapEntry<K,V> { Key(K) Value(V) } class SortedMap<K,V> { def firstEntry() : Option<MapEntry<K,V>> } but everyone probably agrees, that we can figure out that key's are first and values are second, so let's just do this: class Map<K,V> { def firstEntry() : Option<(K,V)> }
- comex 12y agoNo, it doesn't. I think this is the right choice, because when unpacking you need some way to distinguish them anyway, i.e. in match foo { ParseError(e) => ... IntResult(i) => ... StringResult(s) => ... } you need something adorning the left to determine what 'e', 'i', and 's' are; you could use the type, but compared to that it doesn't save much typing to just name the branches (which can always be abbreviated), which avoids issues with multiple variants that happen to have the same type.
- Jweb_Guru 12y agoTo be fair, a similar argument also applies to structs vs. tuples. Unpacking them is really awkward. I suspect language designers only tolerate them because they're so convenient in practice for representing mathematical tuples (where order is actually semantically significant) and have very lightweight syntax in cases where you want to use all or most of the values. But with variants, order is never meaningful and you can only have one disjunctive type at at time (which quashes both those points). There's a Rust RFC which proposes some rather icky syntax for them (match foo { (e|!|!) => ... | (!|i|!) => ... }) and that alone convinced me that this is a no-go.