Continung discussion: @owenhilyard
Unions and sum types are separate concepts. A sum type is checked, which is why we expect there to be control flow involved in accessing it, whereas a union is an unsafe construct with no expectation of safety and thus no need to check things.
What you’re proposing eliminates one of the biggest benefits of sum types, exhaustiveness. When using sum types to specify a communication protocol, the ability to get compiler errors everywhere you need to update to deal with the new variant is valuable.
It’s also very useful to do “deeper” pattern matching, such as
Some(Ok(3)).
Sum type is the wrong name for the right concept.
struct Indirect:
var pointer: OpaquePointer
var type_id: Int
struct Large:
var age: Int
var name: Int
var gender: Int
var tag: Int
struct Small:
var age_name_or_gender: Int # This only works in safe code because the fields are the same type. Here, the name is stored in a string table. Storing it as a string does not work.
var tag: Int
enum Small:
var age: Int
var name: Int
var gender: Int
# 32Bytes vs 16Bytes
union Small:
var age: Int
var name: Int
var gender: Int
# 9Bytes in Struct of Arrays or packed
I don’t know why there are so many different terms for the concept of a safe union.
safe union
tagged union
union (enum)
enum
Variant
Sum Type
Algebraic data type
Any other names?
Union: same offset in memory, size of largest field + padding #.access
Tag: int; can at least hold the number of fields.
fn tag(union: SomeTaggable) -> Int # bultin
The first example is simpler because it does not require references. Even if you don’t know anything about references, you can still mutate the fields of a union without having to argue with the borrow checker.
if name is .success:
name.success += 1
Alternative using references
if name is .success(ref name_success):
name_success += 1
This code would not compile.
if name is .failure:
name.success += 1
This code would not compile, log_name_result does not modify the tag
if name is .success(ref name_success):
log_name_result(name)
name_success += 1
fn log_name_result(read name_result: Result[Name]): ...
You can try unwrap and modify log_name_result, but you might not be able to because they’re external functions.
I hope you see why separating union access checking and borrow checking is a major simplification.
Saying that in one branch, you can get a copy, a mutable reference, or an immutable reference is too limiting.
I think safe unions are the way to go: expressive, no nonsense, natural code that is easily explainable.
They’re just a struct with fields at the same offset and a tag.
They avoid so many complexities, like the match ergonomics in Rust.
Why does Swift have so much sugar for the optional type, and why does Rust have so many methods on Option?
Swift: language complexity
Rust: library complexity
I think the model is flexible enough that all methods on Optional other than value and unsafe_value should be removed if that is implemented.
We don’t need map or flatMap; they make it hard to reason about control flow.
This proposal is simpler to implement because it doesn’t require fancy pattern matching, just union access checking, which is more of a control flow thing.
Python has done a great job with the match statement. I am not proposing to not implement pattern matching.
Exhaustive vs Non-Exhaustive:
Python’s match is not exhaustive.
Mojo could have something like this:
@exhaustive
match x:
case .success(s): pass
case .something_went_wrong(w): pass
Types could be annotated with something like @require_exhaustive.
This would be the best of both worlds: compatibility with Python and low boilerplate.