We wanted to get rid of C++ exceptions but traded it for panic hell.
Switching from C++ to Rust
81–90 of 289 posts
Re: Switching from C++ to Rust
#82Earlier quoted context omitted.
I'm going to make the even stronger claim that a general-purpose statically typed language that doesn't support sum types and exhaustive pattern matching to at least the extent that Rust does is unfit for general use, just like a language that doesn't have product types is unfit for general use. Dynamically-typed languages often have adhoc sum types.
Well that's obviously not true given all the successful apps written in languages that don't support sum types.
Suppose I say that cars without seatbelts are "unfit" to use on public roads. Clearly I can't mean it's impossible to use such cars, people used to do it all the time, but perhaps I mean it's a bad idea to use them, and that's harder to argue with which is why we got laws saying you need seatbelts.
Re: Switching from C++ to Rust
#83Earlier quoted context omitted.
The overhead is the same. Rust just takes care that you're doing it right, while C++ lets you shoot your foot off in this area.
I know this is an unpopular opinion but if you want to stick with C++, the solution to this, at least in my experience, is to stop doing things that let you shoot your foot off. C++ gives you every tool in the tool chest, and most of them are not safe. Stick to a safe subset and you've solved 90% of all those stereotypically C++ problems. I've got a pretty big C++ codebase for my hobby projects, sanded down, polished…
Re: Switching from C++ to Rust
#84Earlier quoted context omitted.
Nope, not unions. Sum types. Sum types would be more analogous to tagged unions or discriminated unions. > i have almost never needed to use such types Well sure. When is an abstraction "needed"? Before Fortran, nobody "needed" a programming language either. So is a programming language necessary? That's the funny thing about the word "need." It has very narrow application, and it's precisely why I didn't mention the…
[flagged]
A tagged union is a specific kind of data structure. It essentially refers to a pair of information: the first is a tag and the second is the union. The union might represent many different kinds of values. The important bit is that the size of that memory is the size of the largest value. The tag then tells you what kind of value is in that memory, and crucially, tells you how to interpret that memory. Getting this wrong, for example, in a language like C can lead to undefined behavior.
Tagged unions show up in all sorts of places. The classical example is an AST. You usually have one "node" type that can be one of many possible things: a literal, a binary operation, a function call, a type definition, whatever. And for any non-primitive node, it usually has pointers to children. And what is the type of a child? Well, a "node"! It's a recursive data type. It lets you represent children as just more nodes, and those nodes might be any other AST node. (And as you might imagine, an AST can be quite a bit more complicated than this. You often want restrictions on which types of children are allowed in certain contexts, and so you wind up with many different tagged unions.)
The use of a tagged union like this can be conceptually thought of as a sum type. A sum type is not a data structure. A sum type is a concept. It comes from type theory and it is the "dual" of product types. Product types represent the conjunction of values while sum types represent the disjunction of values. In type theory notation, you usually see 'A x B x C' to refer to the product type of A, B and C. Similarly, you usually see 'A + B + C' to refer to the sum type of A, B or C.
A sum type can be represented by just about anything, because it's just a concept. You can take something that is typically used for product types and use it to present an API that gives you a sum type:
type OptionInt struct {
exists bool // private
number int // private
}
func OptionIntNone() OptionInt {
return OptionInt{false, 0}
}
func OptionIntSome(number int) OptionInt {
return OptionInt{true, number}
}
func (o OptionInt) IsSome() bool {
return o.exists
}
func (o OptionInt) Number() int {
if !o.exists {
panic("option is none, no number available")
}
return o.number
}
This is a very contrived and hamstrung example, because it really doesn't give you much. But what it does give you is the API of a sum type. It is a value that can be either missing or present with an integer. Notably, it does not rely on any particular integer sentinel to indicate whether the value is missing or not. It encodes that state separately. So it can fully represent the presence of any integer value.As you can see, despite it offering an API that represents the sum type concept, it is actually implemented in terms of a product type, or a struct in this case.
In a language like Go, which does not really support either unions or sum types, this sort of API is really hamfisted. In particular, one could object that it is a sum type because it doesn't really represent the "sum typeness" in the type system. Namely, if you get its use wrong, then you don't get a compilation error. That is, you can call the 'Number()' method without checking whether 'IsSome()' returns true or not. In other words, correct use of OptionInt looks like this:
var opt OptionInt := doSomethingThatMightReturnNone()
if opt.IsSome() {
// OK, safe to access Number. It won't panic
// because we just checked that IsSome() is true.
fmt.Println(opt.Number())
}
But nothing in Go stops you from writing this instead: var opt OptionInt := doSomethingThatMightReturnNone()
fmt.Println(opt.Number())
This is why "support for sum types" is critical for making them as useful as they can be. In Rust, that same option type is defined like this: enum OptionInt {
None,
Some(i32),
}
That's it. The language gives you the rest. And crucially, instead of splitting out the 'IsSome()' check and the access via 'Number()', they are coupled together to make it impossible to get wrong. So the above code for getting a number out looks like this instead: let opt: OptionInt = do_something_that_might_return_none()
if let OptionInt::Some(number) = opt {
println!("{}", number);
}
You can't get at the 'number' without doing the pattern matching. The language won't let you. Now, you can define methods on your sum types that behave like Go's 'Number()' method above and panic when the value isn't what you expect. And indeed, Rust's Option and Result types provide this routine under the names 'unwrap()' and 'expect()'. But the point here isn't just about Option and Result, it's about sum types in general. And you don't have to define those methods that might panic. You can just let the language help guide you along instead.Sum types are an idea. A concept. A technique. Tagged unions are an implementation strategy.
Notice also that I haven't mentioned exhaustiveness at all here. It isn't required for sum types. Haskell, for example, doesn't mind at all if your pattern matching isn't exhaustive. You actually have to tell it to warn you about it, and even then, it's not a compilation error unless you treat it as one. In Rust, exhaustiveness checking is enabled by default for all sum types. But you can disable it for a particular sum type with the '#[non_exhaustive]' attribute. The benefit of disabling it is that adding a new variant is possibly semver compatible change, where as adding a new variant in an exhaustive sum type is definitely a semver incompatible change.
Re: Switching from C++ to Rust
#85Earlier quoted context omitted.
A union in C++ is the same thing as a struct, except all of its fields live at the same offset. So you can define any method you want on it, including special stuff like constructors and destructors. No base classes are allowed, though.
A method on a union is much less useful when you can't match on the tag though. I suppose you could store tag inside the union. Is that common? I've always imagined C/C++ tagged unions would store tag outside the union.
So you could have a struct or class with one std::variant field and some methods which can match on the type of the variant. But it would be kind of clunky.
Re: Switching from C++ to Rust
#86The call out to sum types is something I feel. I've been using Rust daily for almost 10 years now, and sum types are absolutely still one of the things I love most about it. It's easily one of the things I miss the most in other languages that don't have them. I'm usually a proponent of "using languages as they're intended," but I missed exhaustiveness checking so much that I ported a version of it to Go[1] as a sort…
I'm going to make the even stronger claim that a general-purpose statically typed language that doesn't support sum types and exhaustive pattern matching to at least the extent that Rust does is unfit for general use, just like a language that doesn't have product types is unfit for general use. Dynamically-typed languages often have adhoc sum types.
Re: Switching from C++ to Rust
#87The call out to sum types is something I feel. I've been using Rust daily for almost 10 years now, and sum types are absolutely still one of the things I love most about it. It's easily one of the things I miss the most in other languages that don't have them. I'm usually a proponent of "using languages as they're intended," but I missed exhaustiveness checking so much that I ported a version of it to Go[1] as a sort…
[1] unless you liberally use `union`s everywhere which is very unidiomatic C++, but can be done if desired.
Re: Switching from C++ to Rust
#88Earlier quoted context omitted.
Well that's obviously not true given all the successful apps written in languages that don't support sum types.
It depends what you take "unfit" to mean. Suppose I say that cars without seatbelts are "unfit" to use on public roads. Clearly I can't mean it's impossible to use such cars, people used to do it all the time, but perhaps I mean it's a bad idea to use them, and that's harder to argue with which is why we got laws saying you need seatbelts.
Re: Switching from C++ to Rust
#89Earlier quoted context omitted.
A method on a union is much less useful when you can't match on the tag though. I suppose you could store tag inside the union. Is that common? I've always imagined C/C++ tagged unions would store tag outside the union.
i haven't done c++ in a million years, but huh, you can! can't have any types w/ non-trival copy constructors in a union, though, apparently, which is quite the restriction. https://gist.github.com/erinok/c823af95db408653c7e42ab189307...
Re: Switching from C++ to Rust
#90Earlier quoted context omitted.
if that is your example, provide some code.
Pretend we have the following sum type: enum SumType { Foo(foo), Bar(bar), Baz(baz), } void DoSomething(SumType sum_type) { match sum_type { Foo(foo): foo.do_x(); Bar(bar): bar.do_y(); Baz(baz): baz.do_z(); } } This could be lowered to: struct LoweredSumType { optional foo; optional bar; optional baz; } void DoSomething(LoweredSumType sum_type) { if (sum_type.foo.has_value()) { sum_type.foo->do_x(); } else if (sum_ty…
which looks remarkably like a union to me, if i understand your made-up language.