Where have you found so much use for them?
Where have you found so much use for them?
The most motivating example is Rust's Result<T, E>. Either you have a "good" type T, or some error E occurred. Want to bubble this error up? Just do `my_result?` to get the underlying T or return E early. This idiom is pervasive and makes error handling via return codes very easy to use.
It also makes it feasible to have types that have no "default" value. In C++ you have std::unique_ptr<T> which, for various reasons, has to have a nullptr state, or else it's frustrating to use. In Rust, owned pointers Box<T> don't have an empty state. If you need a nullable pointer, you use Option<Box<T>>, which is either Some(my_box) or None. The fact that Box<T> is always pointing to a valid object is a stronger guarantee than C++ smart pointers, and is only ergonomically possible because of the Option<T> sum type.
However your examples i don't think are particularly convincing.
The examples you gave can mostly be done with std::variant<> and are simply examples are unions. (Result == std::expected)
In term of non nullable smart pointer, it would be easy to enforce the the non nullability in the constructor, and have the same ergonomy as Option<Box<T>>. In fact you can do something like std::expected<std::reference<T>> but why would you instead of just std::unique_ptr and handle the nullable case.
What makes sum type powerfull is to have full Algebraic DataType (tags + sum types + product types) + pattern matching + good compiler errors.
It is just that std::optional and unique_ptr do not have the right semantics out of the box, but it is not hard to roll your own.
But yes, I wish C++ has good builtin sum types and proper destructuring via pattern patching. Maybe one day....
It is still possible to return non-movable, non-copyable types from functions.
But I guess ergonomics would not be good.
That's because it comes with a statically-tracked "tag" (so: sometimes called a "tagged union") that the compiler can use to check you. You can write the same thing with a `union` in C or C++, with an `enum` to tag it, but you're doing that by hand rather than having it built in.
Also: Where a struct or class's relationship over its members is the product of all of the types which make it up, a tagged union/enum is the sum of them. A common example: instead of a bunch of optional fields on a single class to model the different possible combinations, modeling something as only the legal set of combinations of fields. I recommend Scott Wlaschin's 'designing with types' series (uses F♯, but applies to any languages with 'sum types') for a great intro and then deep dive on the ideas here: https://fsharpforfunandprofit.com/series/designing-with-type...
https://doc.rust-lang.org/reference/items/unions.html
The unions of Rust are tagged unions that can hold data:
https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html
which is immensely powerful in data modeling.
Some languages do that wrapping and checking for you, automatically. Everything from Ada (variant records) to Haskell has them. You cannot mishandle such objects by misinterpreting them as the wrong type; it's a compile-time error in those languages to do so.
Imagine you are parsing something. In my opinion, the most natural way to express this, is a function that takes an array of characters and returns an array of parsed objects or tokens, where each object may be of a different possible type. In C, you'd probably use a union of structs, and set which type of struct as you parse each object. This is error prone and verbose, and the compiler can't double-check it for you. It's all automatic in a language with sum types. Quite efficiently too; most of the runtime checks can be optimized away.
Sum types are also quite nice to bridge the gap from a 'dynamically typed mindset' into a 'statically typed mindset'. Sum types basically provide the ability that a variable can assume different types during its lifetime (? although I don't know if such 'shape-shifting' is allowed in Rust), so it almost feels like Python or Javascript, but since the compiler 'knows' sum types it's all still completely typesafe.
In Typescript it's convenient and frankly quite nice, in a programming language like Rust where performance matters I'd be less enthusiastic though.
I think so! You mean this?
let mut x: Result <bool, String> = Ok (false);
dbg! (&x);
// Get a reference to the bool and mutate it
if let Ok (b) = &mut x {
*b = true;
}
dbg! (&x);
// "shape-shift" to the other enum variant
x = Err ("No boolean :(".to_string ());
dbg! (&x);
It runs in Godbolt https://godbolt.org/z/q6jqYdYh1 // This derive macro expands to a visitor pattern during build
#[derive (serde::Serialize)]
enum Message {
QueryFoo (QueryFoo),
MutateBar (MutateBar),
}
struct QueryFoo {
foo_id: i64,
}
// If we receive a Message::MutateBar from outside our process,
// it's a compile error for us to accidentally access the field `QueryFoo::foo_id`.
struct MutateBar {
bar_id: String,
baz_value: u32,
}
fn send_msg_as_json (tx: &mut Sender, msg: &Message) -> anyhow::Result <()> {
// The JSON serialization / deserialization is generated, too
tx.send (serde_json::to_string (msg))?;
Ok (())
}
It's possible to represent this in C++, and you can integrate FlatBuffers or whatever to auto-generate the ser/de code, but then you're not writing C++, you're writing some weird DSL and you're fighting the build system and you're teaching juniors how to install it.I haven't seen anything in C++ that's both as easy and as airtight as a Rust enum with serde's automatic serialization macro on the top.
Also in well designed low level binary protocols message types have a discriminant header already so often common to wrap them in an ad hoc-variant which doesn't have a separate discriminant.
In a language with sum types like Rust, Kotlin, and Swift, if you’re following the default “happy path” of coding, you are not allowed to forget a check. The compiler will not let you.
1. failing to check for sigils, because nobody uses a sigil when you have sum types. This would have prevented a famous exploit in SUDO a couple years back where someone forgot to check for the special negative value on an input parameter.
2. Eliminating null pointer exceptions or anything similar. You can't forget to check, you can still choose to have this kind of error, but you have to do it explicitly.
Consider something like a function to open and give you back a file handle. In many languages such a function will return a pointer to a file, or null if there is a failure, or perhaps throw an exception if there is a failure. If you neglect to check for null, or forget to handle the exception, you have a problem. With sum types, the natural thing is to return something like a Result type, which is EITHER a file, or an error. And you have to check which it is, to get the File out.
Additionally that is one of the examples on the ongoing C++ reflection papers.
Then there are a couple of examples in D, Common Lisp, Scheme, C# code generators.
Please point to a specific library if you disagree.