mirror of
https://github.com/Noratrieb/website.git
synced 2026-01-14 17:05:02 +01:00
move submodules in-tree
This commit is contained in:
parent
57c4a239da
commit
830045ee2f
238 changed files with 41955 additions and 187 deletions
263
blog/content/posts/item-patterns-and-struct-else.md
Normal file
263
blog/content/posts/item-patterns-and-struct-else.md
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
+++
|
||||
title = "Item Patterns And Struct Else"
|
||||
date = "2023-03-17"
|
||||
author = "Noratrieb"
|
||||
tags = ["rust", "language-design"]
|
||||
keywords = ["design"]
|
||||
description = "Bringing more expressiveness to our items"
|
||||
showFullContent = false
|
||||
readingTime = true
|
||||
hideComments = false
|
||||
draft = false
|
||||
+++
|
||||
|
||||
# Pattern matching
|
||||
|
||||
One of my favourite features of Rust is pattern matching. It's a simple and elegant way to deal with not just structs, but also enums!
|
||||
|
||||
```rust
|
||||
enum ItemKind {
|
||||
Struct(String, Vec<Field>),
|
||||
Function(String, Body),
|
||||
}
|
||||
|
||||
impl ItemKind {
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Struct(name, _) => name,
|
||||
Self::Function(name, _) => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, we have an enum and a function to get the name out of this. In C, this would be very unsafe, as we cannot be guaranteed that our union has the right tag.
|
||||
But in Rust, the compiler nicely checks it all for us. It's safe and expressive (just like many other features of Rust).
|
||||
|
||||
But that isn't the only way to use pattern matching. While branching is one of its core features (in that sense, pattern matching is just like git),
|
||||
it doesn't always have to be used. Another major advantage of pattern matching lies in the ability to _exhaustively_ (not be be confused with exhausting, like writing down brilliant ideas like this) match over inputs.
|
||||
|
||||
Let's look at the following example. Here, we have a struct representing a struct in a programming language. It has a name and fields.
|
||||
We then manually implement a custom hash trait for it because we are important and need a custom hash trait. We could have written a derive macro, but didn't because
|
||||
we don't understand how proc macros work.
|
||||
|
||||
```rust
|
||||
struct Struct {
|
||||
name: String,
|
||||
fields: Vec<Field>,
|
||||
}
|
||||
|
||||
impl HandRolledHash for Struct {
|
||||
fn hash(&self, hasher: &mut HandRolledHasher) {
|
||||
hasher.hash(&self.name);
|
||||
hasher.hash(&self.fields);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This works perfectly. But then later, [we add privacy to the language](https://github.com/rust-lang/rustup/pull/1642). Now, all types have a visibility.
|
||||
|
||||
```diff
|
||||
struct Struct {
|
||||
+ visibility: Vis,
|
||||
name: String,
|
||||
fields: Vec<Field>,
|
||||
}
|
||||
```
|
||||
|
||||
Pretty cool. Now no one can access the implementation details and make everything a mess. But wait - we have just made a mess! We didn't hash the visibility!
|
||||
Hashing something incorrectly [doesn't sound too bad](https://github.com/rust-lang/rust/issues/84970), but it would be nice if this was prevented.
|
||||
|
||||
Thanks to exhaustive pattern matching, it would have been easy to prevent. We just change our hash implementation:
|
||||
|
||||
```rust
|
||||
impl HandRolledHash for Struct {
|
||||
fn hash(&self, hasher: &mut HandRolledHasher) {
|
||||
let Self { name, fields } = self;
|
||||
hasher.hash(name);
|
||||
hasher.hash(fields);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And with this, adding the visibility will cause a compiler error and alert us that we need to handle it in hashing.
|
||||
(The decision whether we actually do want to handle it is still up to us. We could also just turn off the computer and make new friends outside.)
|
||||
|
||||
We can conclude that pattern matching is a great feature.
|
||||
|
||||
# Limitations of pattern matching
|
||||
|
||||
But there is one big limitation of pattern matching - all of its occurrences (`match`, `if let`, `if let` chains, `while let`, `while let` chains, `for`, `let`, `let else`, and function parameters
|
||||
(we do have a lot of pattern matching)) are inside of bodies, mostly as part of expressions or statements.
|
||||
|
||||
This doesn't sound too bad. This is where the executed code resides. But it comes at a cost of consistency. We often add many syntactical niceties to expressions and statements, but forget about items.
|
||||
|
||||
# Items and sadness
|
||||
|
||||
Items have a hard life. They are the parents of everything important. `struct`, `enum`, `const`, `mod`, `fn`, `union`, `global_asm` are all things we use daily, yet their grammar is very limited. ("free the items" was an alternative blog post title, although "freeing" generally remains a concern of [my C style guide](https://noratrieb.github.io/nilstrieb-c-style-guide-edition-2/)).
|
||||
|
||||
|
||||
For example, see the following code where we declare a few constants.
|
||||
|
||||
```
|
||||
const ONE: u8 = 1;
|
||||
const TWO: u8 = 1;
|
||||
const THREE: u8 = 3;
|
||||
```
|
||||
|
||||
There is nothing obviously wrong with this code. You understand it, I understand it, an ALGOL 68 developer from 1970 would probably understand it
|
||||
and even an ancient greek philosopher might have a clue (which is impressive, given that they are all not alive anymore). But this is the kind of code that pages you at 4 AM.
|
||||
|
||||
You've read the last paragraph in confusion. Of course there's something wrong with this code! `TWO` is `1`, yet the name strongly suggests that it should be `2`. And you'd
|
||||
be right, this was just a check to make sure you're still here. You are very clever and deserve this post. If you didn't notice it, go to sleep. It's good for your health.
|
||||
|
||||
But even if it was `2`, this code is still not good. There is way too much duplication! `const` is mentioned three times. This is a major distraction to the reader.
|
||||
|
||||
Let's have a harder example:
|
||||
|
||||
```
|
||||
const ONE: u8 = 0; const
|
||||
NAME: &
|
||||
str = "nils";
|
||||
const X: &str
|
||||
= "const";const A: () = ();
|
||||
```
|
||||
|
||||
Here, the `const` being noise is a lot more obvious. Did you see that `X` contains `"const"`? Maybe you did, maybe you didn't. When I tested it, 0/0 people could see it.
|
||||
|
||||
Now imagine if it looked like this:
|
||||
|
||||
```rust
|
||||
const (ONE, NAME, X, A): (u8, &str, &str, ()) = (0, "nils", "const", ());
|
||||
```
|
||||
|
||||
Everything is way shorter and more readable.
|
||||
|
||||
What you've just seen is a limited form of pattern matching!
|
||||
|
||||
# Let's go further
|
||||
|
||||
The idea of generalizing pattern matching is very powerful. We can apply this to more than just consts.
|
||||
|
||||
```rust
|
||||
struct (Person, Car) = ({ name: String }, { wheels: u8 });
|
||||
```
|
||||
|
||||
Here, we create two structs with just a single `struct` keyword. This makes it way simpler and easier to read when related structs are declared.
|
||||
So far we've just used tuples. But we can go even further. Structs of structs!
|
||||
|
||||
```rust
|
||||
struct Household<T, U> {
|
||||
parent: T,
|
||||
child: U,
|
||||
}
|
||||
|
||||
struct Household { parent: Ferris, child: Corro } = Household {
|
||||
parent: { name: String },
|
||||
child: { name: String, unsafety: bool },
|
||||
};
|
||||
```
|
||||
|
||||
Now we can nicely match on the `Household` struct containing the definition of the `Ferris` and `Corro` structs. This is equivalent to the following code:
|
||||
|
||||
```rust
|
||||
struct Ferris {
|
||||
name: String,
|
||||
}
|
||||
|
||||
struct Corro {
|
||||
name: String,
|
||||
unsafety: bool,
|
||||
}
|
||||
```
|
||||
|
||||
This is already really neat, but there's more. We also have to consider the falliblity of patterns.
|
||||
|
||||
```rust
|
||||
static Some(A) = None;
|
||||
```
|
||||
|
||||
This pattern doesn't match. Inside bodies, we could use an `if let`:
|
||||
|
||||
```rust
|
||||
if let Some(a) = None {} else {}
|
||||
```
|
||||
|
||||
We can also apply this to items.
|
||||
|
||||
```rust
|
||||
if struct Some(A) = None {
|
||||
/* other items where A exists */
|
||||
} else {
|
||||
/* other items where A doesn't exist */
|
||||
}
|
||||
```
|
||||
|
||||
This doesn't sound too useful, but it allows for extreme flexibility!
|
||||
|
||||
```rust
|
||||
macro_rules! are_same_type {
|
||||
($a:ty, $b:ty) => {{
|
||||
static mut ARE_SAME: bool = false;
|
||||
|
||||
if struct $a = $b {
|
||||
const _: () = unsafe { ARE_SAME = true; };
|
||||
}
|
||||
|
||||
unsafe { ARE_SAME }
|
||||
}};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if are_same_type!(Vec<String>, String) {
|
||||
println!("impossible to reach!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ignoring this suspicious assignment to a `static mut`, this is lovely!
|
||||
|
||||
We can go further.
|
||||
|
||||
Today, items are just there with no ordering. What if we imposed an ordering? (and just like this, the C11 atomic model was born.) What if "Rust items" was a meta scripting language?
|
||||
|
||||
We can write a simple guessing game!
|
||||
|
||||
```rust
|
||||
struct fn input() -> u8 {
|
||||
const INPUT: &str = prompt!();
|
||||
const Ok(INPUT): Result<u8, ParseIntErr> = INPUT.parse() else {
|
||||
compile_error!("Invalid input");
|
||||
};
|
||||
INPUT
|
||||
}
|
||||
|
||||
const RANDOM: u8 = env!("RANDOM");
|
||||
|
||||
loop {
|
||||
const INPUT = input();
|
||||
if INPUT == RANDOM {
|
||||
break; // continue compilation
|
||||
} else if INPUT < RANDOM {
|
||||
compile_warn!("input is smaller");
|
||||
} else {
|
||||
compile_warn!("input is bigger");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Empty. I am useless. I strike!
|
||||
}
|
||||
```
|
||||
|
||||
If it weren't for `fn main` starting a strike and stopping compilation, this would have worked! Quite bold of `fn main` to just start a strike, even though there's no `union` in the entire program. But we really need it, it's not a disposable worker.
|
||||
|
||||
And then, last and least I want to highlight one of my favourite consequences of this: `struct else`
|
||||
|
||||
```rust
|
||||
struct Some(Test) = None else {
|
||||
compile_error!("didn't match pattern");
|
||||
};
|
||||
```
|
||||
|
||||
<sub>you're asking yourself what you just read. meanwhile, i am asking myself what i just wrote. we are very similar.</sub>
|
||||
Loading…
Add table
Add a link
Reference in a new issue