Enums in Rust – and why they feel better
shuttle.rs
shuttle.rs
It's incorrect to describe an enum with two variants as a 'newtype'.
I think the example was misunderstood from https://rust-unofficial.github.io/patterns/patterns/behaviou... (which uses Password, but doesn't use an enum with two variants).
The use of enums in the example is fine; but it's not a "newtype".
You can't normally do this since there's a high risk of duplicate implementations, so by enforcing only local types/foreign traits or foreign types/local traits (and of course local/local types and traits), this duplication doesn't occur - there's only ever one implementation of a trait per type, and the ownership of those implementations is very clear and we'll defined.
The newtype pattern simply makes a "local" type that wraps a foreign type, allowing you to implement the foreign trait on the new local type, and forwarding calls of the trait methods to the inner type - all while maintaining the single trait impl and ownership constraints rust imposes.
Further, given all of the compiler optimizations that happen, newtypes are effectively free.
For those who haven't seen it, this is what newtype looks like:
use some::ForeignType;
struct LocalType(ForeignType);
As the parent comment states, it has nothing to do with enums.For some definition of "mostly". I use most of my newtypes neither in Rust nor in Haskell to implement traits, but most of the time to declare - newtypes ;) that wrap other (primitive) types.
I agree that this could have been worded better - I'll make an adjustment to change this.
- tagged unions - sum types - algebraic datatypes - variants
There are some rust packages that transform rust enums into proper enums, the best one being const_table, or my own table_enum. Java got it right except for bundling the data with the Enum object instead of storing it in separate tables.
Having said that, I could not be happier that sum types have entered the mainstream.
Because Rust is a low bit-banging language, union needs to exist, which I think rules out calling enum "tagged unions" even if that's one possible way to look at the implementation. In a high level language which lacks union types this wouldn't an issue, but Rust is not that language.
Better names would be "tagged union" (which is spot-on descriptive - because it *is* a union with a tag slapped on - but "tagged_union" of course is a bit unwieldy as keyword) or "variant" (which is the name for that thing in C++ and elsewhere since forever - it's not like Rust invented those tagged-union-thingies, it just added some convenient syntax sugar to the language to deal with them).
` (* enum ) type 'a Option = Some of 'a | None `
( struct - well: record. *) type Company = { Name : string Age : int }
Fudge. No idea how to format that in HN.
I really wonder how that decision making progress looked like :)
Why not call Enum Union, and the FFI one CUnion or something? Like, is there something meaningful that Union is for other than C interop? Isn't it functionally identical to a tagged union in every other respect?
No :)
> is there something meaningful that Union is for other than C interop?
Look more closely at the Rust standard library, MaybeUninit<T> is a union.
The use of "enum" in Rust predates the standardization of C++17, where std::variant was introduced.
https://www.boost.org/doc/libs/1_64_0/doc/html/variant.html
The name and concept was also common enough to that I used "Variant" in my own C++ utility class collection around 2005 or so.
But it's not, hence the aforementioned bikeshedding. When Rust enum variants have data, they're like C unions with automatically-inserted tag checking. When Rust enums don't have data, they're like C enums (they're literally how you do FFI interop with enums in C, which should demonstrate beyond a shadow of a doubt that `enum` isn't categorically wrong). They have the properties of both depending on how they're defined, but at the end of the day they technically have more in common with C enums than C unions (which is why Rust also has `union` for doing C FFI).
> to get C++ programmers on board, wouldn't the obvious name not be "variant"?
Most early Rust contributors were also C++ programmers (due to Mozilla), and I don't recall a single person ever suggesting `variant` as the keyword. I suspect people here are overestimating the mindshare of boost::variant as of 2014.
"OK, let's not go with that then"
"C calls it union"
"OK, we DEFINITELY can't call it that"
"Enum" is not the worst term here. In fact you are enumerating the variants. It's only slightly misleading. It implies that you're comparing an integer.
There is an actual enum backing it to encode the choice (tag), and a union to encode the data (if it has data). Or at least that's what I would expect.
"Variant" is used in Rust lingo to name the variants, not the whole type.
I didn’t write a lot of compilers in Rust yet, but I find it limiting that you can’t pattern match across a Box<T>, and all ASTs require boxing for recursive references.
[0] https://doc.rust-lang.org/beta/unstable-book/language-featur...
Box patterns are likely to be superseded by one of these:
what a pleb
(Note that this doesn't work if your "arena" is just a Vec, which is something people have sort of hijacked the term "arena" for. It needs to be an arena that doesn't reallocate its contents on growth, something like https://docs.rs/bumpalo/latest/bumpalo/, in order to hand out references that live across allocations like this.)
Here's my example of sum types being implemented in julia as a regular package: https://github.com/MasonProtter/SumTypes.jl
The important thing here is that the tags are fundamental to the subtyping relations. So unless your language is flexible enough to let one redefine what it means to be a subtype, then there's no chance of making unions out of tagged unions.
TaggedUnion{A, B} does not have the property that A <: TaggedUnion{A, B}, whereas with regular unions you must have that property, which is a very hard thing to hack into a language after the fact.
This is nonsense. I doubt that this is common feedback and it is just not correct that it is far better supported in Rust than in any other language. Why would anyone say that? Rust pretty much has support for ADTs, sum types and product types. Just like any modern (functional) language has.
Haskell, Scala, F#, OCaml, etc.
(And I love rust, write it in my day-to-day as well as on my spare time.)
Java, C, C++, python, Javascript, go...
As a second language is a quite different selector than after education in language design or excursions to functional programming.
They are real objects, can have associated data and methods.
So if they are surprised by Rust, haven't learnt Java properly
Though from a quick search, it's still just scalar/unary values. Rhough one of the better implementations of those.
But the big advantage of the more sum type Style enums rust provides is the ability to nest values in them while preserving type safety guarantees. So I still see them as miles better as a language concept.
Rust enums are sum types, which means that each variant can have a totally different shape and contain different data, and it's the user of the enum that supplies the data when they create an instance of a variant. This quintessential use case for sum types not possible with Java enums (although it is now sort of possible with records and sealed traits):
enum Result<T, E> {
Ok(T),
Err(E)
}The thing in Java that is closest to Rust enum's would be sealed classes, which was a preview feature in Java 15 and finalized and released with Java 17.
For the real deal, there are sealed classes now, modeled on how Scala does it.
For reference, they can be modeled as a "sealed" class since Java 17. https://openjdk.org/jeps/409
Prior to Java 17, sealed classes feature can be partially recreated via private constructors in the super classes. This approach forces you to put all classes in a single Java file, which can be awkward to navigate.
Rust is the first not-weirdass-academic-wtf language to have these features ;)
(I love functional programming and apply the functional approach no matter what language I’m using - but why is it that functional languages are always so deeply down the academic rabbithole that they think eg “car” and "cdr” are intuitive names for “retrieve the first element of a list” and “retrieve the rest of the list” functions??)
Lisp pairs are flexible objects that can be coupled together into shapes and uses that are not lists. car and cdr can mean first and rest, and ANSI Common Lisp [1994] has those synonyms, but there are uses of cons cells where first and rest do not make sense.
MacCarthy must have realized that words that do not invoke any connotations are good for the elements of a flexible pair structure. Choices like first/rest, left/right, top/bottom and others are saddled with semantics that don't match every use.
Programs that need a pair structure in which the two pieces have very specific roles can provide their own synonyms, if their authors feel it makes code more readable.
I seem to recall that Knuth, in TAOCP, at one point, calls the pointers of a binary tree node ALINK and BLINK.
Car and crd is lisp and from the 60:ies, basically. You can liken them to assembly instructions because that's kinda sorta why they're named like that.
I still stand by what I said about them.
If you write Rust for 1-2 years, I bet you you'll come to look at programming in those languages completely differently. I have a very high carry over, going in to Rust from those languages.
(I come from C/Embedded world)
To me, enums are one of the great things about Rust, and I wish I had the same capabilities in JavaScript and other languages.
Most people have not had any exposure to functional programming languages or functional programming in general. When they say "any other language" they mean, any other language that they have used or know anything about I guess.
This is one of those cases where we fail to realize that other people's reality is totally different than ours. Everyone does this all of the time, not just you specifically to be clear, this is a limitation in our brains.
This misses the important point that Go lacks pattern matching -- one uses a switch statement instead -- and the Go compiler lacks the ability to check that all branches have been handled in a switch statement.
Go's concurrency stuff is beautiful. If Go had sum types / Rust-style enums with pattern matching and handled errors as a Result sum type, then it would be a much nicer language.