Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

A sibling comment already mentioned the type system as a whole, but I wish to highlight one specific feature: Rust has algebraic data types.

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.



> I honestly can't see a modern, programmer-friendly language not having proper discriminated union types in 2024

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?


`std::variant` is very awkward to use, and has design compromises because it doesn't have language support. Besides, the ability to write something like `std::variant` as a pure library type first requires you to have a very complex type system, more complex than Rust's, and certainly more complex than a "simple" language such as C or Go would ever consider adopting.

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.


Right, I'm not trying to argue that std::variant is better than native language support. By definition, a language construct will always be easier to write and read.

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.


You did ask more generally:

> 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's language-level support for pattern matching (including exhaustiveness checking) is very nice. Most languages with sum types have this feature. It's hard to imagine one without the other. I haven't used `std::variant` much, but I remember finding it unergonomic. Real-world C++ code uses `std::variant` way less often than Rust/functional code uses sum types. Probably, for that reason

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.


I agree, the lack of pattern matching is a bummer. You should retry std::variant with the lambda overload trick though. It's kind of okay in terms of syntax.

    using MyDiscreminatedUnion = std::variant<MyType1, MyType2, MyType3>;

    MyDiscreminatedUnion var = MyType2{};

    std::visit(overload {
        [](MyType1 t1) {...},
        [](MyType2 t2) {...},
        [](auto t) {...},
   }, var);


You can even do better with something like:

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) {...}
);




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: