This is one most valuable aspects of Rust as language for me.
I feel like it's often overlooked or taken for granted and/or unbreakable.
While in reality it needs to be looked after, protected, hardened.
It's one of the things that differentiate Rust from so many other languages.
It's what gave me the confidence to go all-in on Rust (coming from C/C++/Swift) when I found out about it as a language and trust that it will not turn into one of those many "everything and the kitchen sink"-languages.
Yes, Rust is a complex language. But no, Rust is not a complicated language.
Rust's syntax as well as semantics are based upon a small and easily memorizable set of orthogonal components.
Let me give an example.
(Please excuse the length and what might seem like going off-topic for a bit. It's not. promised.
)
(For the sake of this example allow me to assume that this Pre-RFC ("unnamed struct types") had landed by now.)
In Rust there is a hierarchy of types:
Unnamed types (tuples)
An unnamed type can have zero, n indexed or n named members:
()(T, U, …){ t: T, u: U }
Named types (structs)
A named type is a combination of one of those three kinds of unnamed types, plus a name:
struct Unit(Rust omits the()here)struct Indexed(T, U, …)struct Named { t: T, u: U }
Once you know tuples, you know structs.
They can be thought of as "just named tuples".
Sum types (enums)
A sum type is a combination of one or more named types:
enum Foo {
Unit,
Indexed(T, U, …),
Named { t: T, u: U },
}
Once you know structs, you know enum variants.
They can be thought of as "just unions over structs",
that need to be pattern matched to access.
While this example may seem completely unrelated to the topic at hand.
Lot's of things fall out of this:
Pattern Matching
Matching on tuples:
match () {
() => …,
}
match ("indexed", true) {
(name, ..) => …,
}
match ({ name: "named", flag: true }) {
{ name, .. } => …,
}
Once you know how to pattern match on tuples,
you know how to pattern match on structs:
match Unit {
Unit => …,
}
match Indexed("indexed", true) {
Indexed(name, ..) => …,
}
match (Named { name: "named", flag: true }) {
Named { name, .. } => …,
};
Once you know how to pattern match on structs,
you know how to pattern match on enum variants:
match Foo::… {
Unit => …,,
Indexed(name, ..) => …,,
Named { name, .. } => …,,
}
By making use of carefully chosen combinations of orthogonal language features
Rust allows one to apply very few rules (with almost no exceptions!!!)
to a broad range of uses, such as:
- Every type can be destructured/pattern-matched.
Tuples, structs, enums. No secret sauce necessary. - Every type can implement functions.
Tuples, structs, enums. No secret sauce necessary. - Every type can be used as reference or value type.
Tuples, structs, enums. No secret sauce necessary. - Every type can be used as mutable or immutable value.
Tuples, structs, enums. No secret sauce necessary. - Every type can be <enter characteristic here>.
Tuples, structs, enums. No secret sauce necessary.
Mutability and reference-typed-ness are realized through composition, rather than by introducing special cases. Same goes for memory management, atomicity, … you name it.
… see a pattern here? Rust is composition all the way down. ![]()
It's these clearly visible patterns and the lack of exceptions to most rules that make Rust,
while with no doubt one of the more complex languages, such a joy to learn, teach and use.
Learning patterns scales linearly by O(n + m). (
Rust, a complex language)
Learning cases/exceptions scales by squared by O(n * m) (
Swift, a complicated languages)
Instead of learning how to destructure/pattern-match, call functions on, borrow, … tuples,
and then having to learn a completely different set of rules for structs,
and yet more and utterly different rules for enum variants,
one just has to learn it once for the lowest abstraction and then build the higher ones from it.
I don't know any other similarly complex language with such a rich set of type-flavors that still manages to not require one to memorize a bunch of exceptions and sugars for every single one of its rules/parts, as is the case for most other non-trivial languages.
So what does all this have to do with catching functions, again?
This:
Syntactic sugar that simplifies the use of some things, at the cost of obfuscating their true nature have the ugly downside of making it impossible for the observer to see the language patterns that one would otherwise find easily. Without patterns one is forced to memorize every single configuration separately as individual cases, again blurring the semantics that unify them. This is what happened to Swift.
Catching functions —I fear— risk becoming one of those sugars, that do more harm than good.