The term sounds academical, but I honestly can't see a modern, programmer-friendly language not having proper discriminated union types in 2024. Go's lack of sum types is not simplicity. It's a glaring omission, forcing programmers to rely on "idioms" like using tuples to return errors. Having only product types (structs) is literally like trying to do arithmetic with only multiplication, without addition.
Does that really need to be part of the language though, or as long as you can code it, or have it in the standard library, it's fine?
What can Rust's unions do that std::variant cannot?
UPDATE: also, some advantages coming from the interaction of enums with other Rust language features:
- Because Rust doesn't have a stable ABI, the compiler is free to agressively optimize the layout of enums. The size of an enum is often equal to (rather than greater than) the size of the largest variant, if the variant has some "impossible" bit values (like null pointers and non-UTF8 chars) that can be used for storing the tag.
- Because Rust traits can (and must) be implemented outside of the type definition, you can implement a trait directly for an enum and then use that enum as a polymorhpic "trait object". In C++, if you want to polymorphically use an `std::variant` as a subclass of something, you need to define an ackward wrapper class that inherits from the parent.
using MyDiscreminatedUnion = std::variant<MyType1, MyType2, MyType3>;
MyDiscreminatedUnion var = MyType2{};
std::visit(overload {
[](MyType1 t1) {...},
[](MyType2 t2) {...},
[](auto t) {...},
}, var);namespace StlHelpers {
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;
template<class var_t, class... Func> auto visit(var_t & variant, Func &&... funcs)
{
return std::visit(overload{ funcs... }, variant);
}}
And then
StlHelpers::visit(var,
[](MyType1 t1) {...},
[](MyType2 t2) {...},
[](auto t) {...}
);C++ is going to have pattern matching "any year now", and it's going to make `std::variant` more ergonomic to use, but on the other hand any pattern matching feature will have to make design compromises in order to support `std::variant` and other mutually-incompatible variant-like library types of which C++ has a bunch of (pointers, unions, `std::optional`, `std::any`, `std::expected`, did I miss any?) Add to that all the zillions of third-party variant-like types (including boost::variant) that exist in the wild because the code base predates C++17 and/or doesn't want to use C++17 features for whatever reason. All this complexity could've been avoided had the language just supported real sum types from the start. Or at least from C++11 up or whatever.
As a sibling commenter noted, algebraic data types and pattern matching with compile-time exhaustiveness checking go hand in hand; I meant to mention the latter in my original comment, but left it out because I consider the latter almost implied by the former.
All I'm saying is that the current state of std::variant makes it okay enough to use type safe discriminated unions.
Visit + overload is not that far away from pattern matching in terms of readability, clang does warn on non exhaustive switch cases, etc.
> Does that really need to be part of the language though, or as long as you can code it, or have it in the standard library, it's fine?
And my answer is, yes. I don't consider `std::variant` a proper replacement, more like a crutch that may even be worse than not having anything at all, because its existence can be used as an argument against introducing language-level sum types in the future.
* Rust offers memory safety without a GC. You may or may not want a GC. If you don't, then Rust is the better option.
* More broadly, Rust has a C++-like focus on providing zero cost abstractions. Go is generally happy to accept a small runtime cost for abstraction.
* Rust can generate small WASM targets because it doesn't need to bundle a runtime. Go can target WASM too, but you either need to accept much larger object sizes or use TinyGo, which doesn't implement all Go language features (although it's pretty close now that generics support has arrived).
* Rust code generally runs faster (although a lot depends on whether you're writing the kind of code where a GC is a net positive or a net negative for performance).
* Rust has a fancier type system that's much more able to express invariants. If your happy place is a place where the type system proves that your code is correct, then Rust will make you much happier than Go. The Go type system (and the culture around Go more generally) tends not to favor elaborate abstractions built on types.
* Rust has fully-featured macros, if that's your bag.
I think there are also some disadvantages of Rust compared to Go, but I've only attempted to list the advantages here.
- No more bugs where a value that shouldn't be mutated is accidentally mutated in another place.
- No more bugs with writing to a closed channel or file.
- No more bugs with forgetting to close a file.
- No more null pointer errors at runtime.
- No more runtime reflection errors.
Compared to Go, safe Rust provides all these benefits and more.
This is the most glaring thing IMO. Go repeating the billion dollar null pointer mistake is inexcusable IMO. There’s zero reasons for a language designed in the last 20 years to have this problem. This alone is enough for me to want to stay away from Go.
- Do the things C and C++ can do.
- Without the memory corruption issues those languages are infamous for.
- With the conveniences you'd expect of any post-internet language. (A library ecosystem that's unified around a standard build system and package manager, an async IO story, UTF-8 strings, etc.)
I feel that this could have been provided even without having Cargo and crates repeat the mistakes of both Maven and NPM.
At least the async IO is nice enough even if it does rely on a bunch of sometimes uncontrollable heap allocation. I'd prefer CSP personally, but it could be worse. Although with that you also couldn't avoid allocations.
So to be fair, I was incorrect about it being a combinatorial explosion, since I was under the impression that each combination of features would be a package, but this makes a lot more sense. It's still a quite foreign way of packaging software, though. Although I'm glad that at least Cargo can be operated offline and from official repos.
Do fedora and apt try to mirror all of the packages from crates.io? Are they kept up to date? Is this a manual process, where a human picks and chooses some packages and hopes nothing is missing, or is it a live mirror? If it’s done by hand, what are the chances all the dependencies for some given project will even be available?
It may cause some amount of duplication across all packages, but the final amount is arguably will be quite small when measured in MB. Also new release of an upstream crate may cause several updates of downstream packages even if downstream apps did not release new versions, but distros are not known for quick updates either way, so it should not be a big issue.
The package managers in distros are pretty awful for a language like rust though. They are designed for dynamically linked C code, not a language like rust where small, developer published libraries are the norm and there’s no dynamic linking. Distro package managers also don’t support rust’s feature flags well (C programs with compile time config often has the same problem).
Apt, rpm and friends’ biggest problem is they’re awful for developers. If I write a program or library for people to use, now I’m expected to test and keep up-to-date packages (or at a minimum build instructions) for like, 6 different operating systems. “On Debian, apt install packages X and Y. Z is also needed but it’s out of date so install that from source. On Ubuntu it’s nearly the same but library Z is usable in apt. On redhat everything is available but named differently. And gentoo. And arch. And nixos. And FreeBSD pkg. Also here’s the configure script. And CMake, visual studio project files, Xcode project files, homebrew and a windows installer too.
What version of rust is even available on Debian and redhat? Is it 2 months old or 2 years old? Do my rust project’s dependencies work on that version of rustc? Are they available in apt? Urgh just kill me.
Cargo means I can just ship my project in the form I use while developing. Users get all the latest packages, chosen by me, no matter their OS. And I know their build environment is sane. Hate on cargo if you want, but cargo, npm and friends are the only sane way to ship cross platform code.
If you can write pure Rust in a single library/binary these aren't major issues but as a drop-in replacement for C/C++ in many of the areas where those languages are heavily used today, the edges can be surprisingly sharp.
* safe by default — for example all array accesses are checked by default. You can do unchecked access for speed when you need, and you can check for safety in C and C++ if you want, but I feel nowadays we really should be using “safe by default”.
* much easier to parallelize. I had basically given up on multithreading in C++ and believed it was almost impossible to do well. In Rust, in my experience, if parallel code compiles it works correctly. This is because the borrow checker stops you ever mutating anything in two threads at once at compile time.