Learning Rust with Advent of Code 2023 - Princeton

HenrySchreiner 192 views 43 slides Jul 23, 2024
Slide 1
Slide 1 of 86
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58
Slide 59
59
Slide 60
60
Slide 61
61
Slide 62
62
Slide 63
63
Slide 64
64
Slide 65
65
Slide 66
66
Slide 67
67
Slide 68
68
Slide 69
69
Slide 70
70
Slide 71
71
Slide 72
72
Slide 73
73
Slide 74
74
Slide 75
75
Slide 76
76
Slide 77
77
Slide 78
78
Slide 79
79
Slide 80
80
Slide 81
81
Slide 82
82
Slide 83
83
Slide 84
84
Slide 85
85
Slide 86
86

About This Presentation

A talk given to the Princeton RSE group over learning Rust using the Advent of Code.


Slide Content

Learning Rust
Henry Schreiner 1-10-2024
with Advent of Code 2023

What sort of language is Rust?

What sort of language is Rust?
High level or low level?

What sort of language is Rust?
High level or low level?
Object Oriented?

Low Level
Allowed in Linux Kernel (C)
Native embedded support
No exception handling (C++)
No garbage collector (Go)
High Level
Zero cost abstractions (C++)
Functional elements
Trait system
Syntactic macros

OOP? Depends…

OOP? Depends…
No inheritance

OOP? Depends…
No inheritance
Traits (like Protocols/Interfaces)
Syntactic macros

Why Rust?

Why Rust?
Packaging

Why Rust?Packaging
Modern without legacy (C++23 - C++98)

Why Rust?Packaging Modern without legacy (C++23 - C++98)
Memory Safety (compile time)

Why Rust?Packaging Modern without legacy (C++23 - C++98) Memory Safety (compile time)
Explicit error handling

Why Rust?Packaging Modern without legacy (C++23 - C++98) Memory Safety (compile time) Explicit error handling
Modern tech stacks

Why Rust?Packaging Modern without legacy (C++23 - C++98) Memory Safety (compile time) Explicit error handling Modern tech stacks
Traits

Why Rust?Packaging Modern without legacy (C++23 - C++98) Memory Safety (compile time) Explicit error handling Modern tech stacks Traits
Declarative programming

Why Rust?Packaging Modern without legacy (C++23 - C++98) Memory Safety (compile time) Explicit error handling Modern tech stacks Traits Declarative programming
Crabs are cuter than snakes

Ruff: an exemplary project
0s 20s 40s 60s
Ruff
Autoflake
Flake8
Pyflakes
Pycodestyle
Pylint
0.29s
6.18s
12.26s
15.79s
46.92s
> 60s

Cargo

Cargo
Simple configuration
[package]
name = "foo"
version = "0.1.0"
edition = "2021"
[dependencies]

CargoSimple conf
Dependencies work!
[package]
name = "foo"
version = "0.1.0"
edition = "2021"
[dependencies]

CargoSimple conf Dependencies work!
Locks by default

CargoSimple conf Dependencies work! Locks by default
IDEs support it

CargoSimple conf Dependencies work! Locks by default IDEs support it
Everyone uses it

CargoSimple conf Dependencies work! Locks by default IDEs support it
Everyone uses it
Also rustup:
Easy version selection
& nightlies

Cargo commands

Cargo commands
New
cargo new foo
Created binary (application) `foo` package

Cargo commandsNew
Run
cargo new foo
Created binary (application) `foo` package
cargo run --bin foo
Compiling foo v0.1.0 (/Users/henryschreiner/tmp/foo)
Finished dev [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/foo`
Hello, world!

Cargo commandsNew Run
Tests
cargo test
Compiling foo v0.1.0 (/Users/henryschreiner/tmp/foo)
Finished test [unoptimized + debuginfo] target(s) in 2.53s
Running unittests src/main.rs (target/debug/deps/foo-f51fd73e2da0c0ec)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo commandsNew Run Tests
Benchmarking
cargo test
Compiling foo v0.1.0 (/Users/henryschreiner/tmp/foo)
Finished test [unoptimized + debuginfo] target(s) in 2.53s
Running unittests src/main.rs (target/debug/deps/foo-f51fd73e2da0c0ec)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
cargo bench
Compiling foo v0.1.0 (/Users/henryschreiner/tmp/foo)
Finished bench [optimized] target(s) in 0.20s
Running unittests src/main.rs (target/release/deps/foo-140fab48e69ea289)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargo commandsNew Run Tests Benchmarking
Docs

Cargo commandsNew Run Tests Benchmarking Docs
Formatting
cargo fmt

Cargo commandsNew Run Tests Benchmarking Docs Formatting
Linting
cargo clippy --fix --all
Checking foo v0.1.0 (/Users/henryschreiner/tmp/foo)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
cargo fmt

Cargo commandsNew Run Tests Benchmarking Docs Formatting Linting
Plugins
Expand
Show code generation
Unused features
Find features not being used
Nextest
Fancier testing interface
Audit
Find security vulnerabilities
Udeps
Find unused dependencies
Readme
Make README.md from docs

Demo
New package
Photo by Redaviqui Davilli on Unsplash

Modern design
no legacy
Enums
Modern enum +
std::variant (C++17)
Enum + Union (Python)
Use statements (including *)

Modern design
no legacy
Enums
Modern enum +
std::variant (C++17)
Enum + Union (Python)
Use statements (including *)
enum Direction {
North,
West,
East,
South,
}

Modern design
no legacy
Enums
Modern enum +
std::variant (C++17)
Enum + Union (Python)
Use statements (including *)
enum Direction {
North,
West,
East,
South,
}
enum Value {
Int(i32),
Float(f64),
}

Modern design
no legacy
Enums
Modern enum +
std::variant (C++17)
Enum + Union (Python)
Use statements (including *)
enum Direction {
North,
West,
East,
South,
}
enum Option<T> {
Some(T),
None,
}
(Don’t actually write this, it’s built in!)
enum Value {
Int(i32),
Float(f64),
}

Modern design
no legacy
Pattern matching
Perfect with enums
Exhaustive
Shortcut “if let”

Modern design
no legacy
Pattern matching
Perfect with enums
Exhaustive
Shortcut “if let”
let val = match dir {
North => 1,
West => 2,
East => 3,
South => 4,
}

Modern design
no legacy
Pattern matching
Perfect with enums
Exhaustive
Shortcut “if let”
let val = match dir {
North => 1,
West => 2,
East => 3,
South => 4,
}
mmmmm

pppppppp(""v}}))
}

Modern design
no legacy
Pattern matching
Perfect with enums
Exhaustive
Shortcut “if let”
let val = match dir {
North => 1,
West => 2,
East => 3,
South => 4,
}
if let Some(v) = opt {
println!(“{v}");
}

Modern design
no legacy
Error handling
No exceptions (catching)
Panic (uncatchable exit)
Can unwind (default) or abort
Error enum (C++23)
Shortcut: ?
(on Option too)

Modern design
no legacy
Error handling
No exceptions (catching)
Panic (uncatchable exit)
Can unwind (default) or abort
Error enum (C++23)
Shortcut: ?
(on Option too)
panic!("Goodby");

Modern design
no legacy
Error handling
No exceptions (catching)
Panic (uncatchable exit)
Can unwind (default) or abort
Error enum (C++23)
Shortcut: ?
(on Option too)
panic!("Goodby");
fn f(x: i32) -> Option<u32> {
if x >= 0 {
Some(x as u32)
} else {
None
}
}

Modern design
no legacy
Error handling
No exceptions (catching)
Panic (uncatchable exit)
Can unwind (default) or abort
Error enum (C++23)
Shortcut: ?
(on Option too)
panic!("Goodby");
fn f(x: i32) -> Option<u32> {
if x >= 0 {
Some(x as u32)
} else {
None
}
}
fn g(x: i32) -> Option<u32> {
Some(f(x)?)
}

Modern design
no legacy
Moves
Move (C++11) by default
Explicit clones & references
Slices (C++17)

Modern design
no legacy
Moves
Move (C++11) by default
Explicit clones & references
Slices (C++17)
let s = "hello".to_string();

Modern design
no legacy
Moves
Move (C++11) by default
Explicit clones & references
Slices (C++17)
let s = "hello".to_string();
f(s);
// s now invalid!

Modern design
no legacy
Moves
Move (C++11) by default
Explicit clones & references
Slices (C++17)
let s = "hello".to_string();
f(s);
// s now invalid!
f(s.clone());
// Explicit copy
// Some types support implicit

Modern design
no legacy
Moves
Move (C++11) by default
Explicit clones & references
Slices (C++17)
let s = "hello".to_string();
f(s);
// s now invalid!
f(s.clone());
// Explicit copy
// Some types support implicit
f(&s);
// Immutable reference

Modern design
no legacy
Moves
Move (C++11) by default
Explicit clones & references
Slices (C++17)
let s = "hello".to_string();
f(s);
// s now invalid!
f(s.clone());
// Explicit copy
// Some types support implicit
f(&s);
// Immutable reference
Functions can even take &str,
which is a generic string slice!

Modern design
no legacy
Constraints required
Constraints (C++20) required
Great error messages

Modern design
no legacy
Constraints required
Constraints (C++20) required
Great error messages
use std::vec::Vec;
fn sum<T>(v: &Vec<T>) -> T {
v.iter().sum()
}

u╰╰
u╰╰
f_

}
Modern design
no legacy
Constraints required
Constraints (C++20) required
Great error messages
use std::vec::Vec;
fn sum<T>(v: &Vec<T>) -> T {
v.iter().sum()
}

u╰╰
u╰╰
f_

}
Modern design
no legacy
Constraints required
Constraints (C++20) required
Great error messages
use std::vec::Vec;
fn sum<T>(v: &Vec<T>) -> T {
v.iter().sum()
}
Entirely following instructions
from the compiler!

Modern design
no legacy
Constraints required
Constraints (C++20) required
Great error messages
use std::vec::Vec;
fn sum<T>(v: &Vec<T>) -> T {
v.iter().sum()
}
use std::iter::Sum;
fn sum<T: for<'a> Sum<&'a T>>(v: &[T]) -> T {
v.iter().sum()
}
Entirely following instructions
from the compiler!

Modern design
no legacy
Module system
Modules (C++20, C++23)
Explicit public interface
(private by default)

Modern design
no legacy
Module system
Modules (C++20, C++23)
Explicit public interface
(private by default)
Basically nothing to show, it just works.
No import statements required.

Memory safety
at compile time!
Ownership
Tracked at compile time
Lifetimes
Always tracked, sometimes explicit
Single mutable ref
Can’t have a const ref too!
Unsafe blocks
Can move safety checks to runtime
Only one of the three major C++ successor
languages (Val) is attempting this!
Library utilities
Rc, Weak, Box, Cell, RefCell, …

Modern needs
designed in!
Great Python Support
WebAssembly
Embedded systems
Single binary
No GLIBC required
Cross-compiles

Syntactic Macros
Rust’s secret sauce
Almost like reflection (C++26?)
Fully scoped
Support variable arguments
Simple inline version too
Can be written in Rust!
Access tokenizer and AST
println!("Hello")
vec![1,2,3]
#[derive(Debug)]
struct A {}
#[cfg(test)]
struct A {}
All items can have attributes, too!

Declarative programming
AoC Day 10
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumString, strum::Display)]
enum MapChar {
#[strum(serialize = "S")]
Start,
#[strum(serialize = "|", to_string = "│")]
Vertical,
#[strum(serialize = "-", to_string = "─")]
Horizontal,
#[strum(serialize = "J", to_string = "╯")]
UpLeft,
#[strum(serialize = "L", to_string = "╰")]
UpRight,
#[strum(serialize = "7", to_string = "╮")]
DownLeft,
#[strum(serialize = "F", to_string = "╭")]
DownRight,
#[strum(serialize = ".", to_string = "•")]
Empty,
}
7-F7-
.FJ|7
SJLL7
|F--J
LJ.LJ
╮─╭╮─
•╭╯│╮
S╯╰╰╮
│╭──╯
╰╯•╰╯

Traits
Protocol++
use core::ops::Add;
#[derive(Debug)]
struct Vector {
x: f64,
y: f64,
}
impl Add for Vector {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let a = Vector{x: 1.0, y: 2.0};
let b = Vector{x: 3.0, y: 4.0};
let c = a + b;
// Or let c = a.add(b);

println!("{c:?}");
}
Explicit opt-in
Method-like syntax
Derive macro
Explicit scoping
Rule: Must “own”
either trait or type

What is Advent of Code?
Just in case you haven’t heard of it
•25 structured word problems expecting code-based solutions
•Example test input
•A per-user unique generated input
•A (usually large) integer answer, time spaced retries
•A second part unlocked by answering the first, same input
•Competitive ranking based on time to solution, released at midnight
•~1/4 million people attempt every year, 10K or so finish
•Often unusual variations on known problems (helps avoid AI solutions)

Advent of Code to learn a language
Pros and cons
•Code should solve problems
•Wide range of algorithms
•Extensive solutions available after the leaderboard fills
•Libraries not required but often helpful
•Nothing too slow if done right
•All share similar structure (text in, int out)
•Hard to be competitive in a new language

henryiii/aoc2023
Repository details
Solutions to all 25 days of AoC 2023 in Rust
Most are stand-alone solutions (a couple share via lib)
Balance between speed and readability
Trying different things, libraries, styles
Some documentation
Some have Python versions in docs
Many have various versions in Git history

Day 1
Sum first + last
fn number_line(line: &str) -> u32 {
let mut chars = line.chars().filter_map(|c| c.to_digit(10));
let start = chars.next().unwrap();
let end = chars.last().unwrap_or(start);
10 * start + end
}
fn main() {
let text = std::fs::read_to_string( "01.txt").unwrap();
let sum: u32 = text.lines().map(number_line).sum();
println!("Sum: {sum}");
}
#[cfg(test)]
mod tests {
use super::*;
const INPUT: &str = "\
1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchetn";
#[test]
fn test_01() {
let sum: u32 = INPUT.lines().map(number_line).sum();
assert_eq!(sum, 142);
}
}
Only docs are removed,
otherwise this is the
whole file!
src/bin/01.rs gets
picked up automatically
by Cargo

Day 5
Optional progress bar
#[cfg(feature = "progressbar")]
use indicatif::ProgressIterator;
/// …
#[cfg(feature = "progressbar")]
let seed_iter = seed_iter.progress_count(
seeds.iter().skip(1).step_by(2).sum()
);

Day 7
Card game
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, EnumString)]
enum StdCard {
#[strum(serialize = "2")]
Two,
#[strum(serialize = "3")]
Three
// …
} // Also JokerCard

Day 7
Card game
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, EnumString)]
enum StdCard {
#[strum(serialize = "2")]
Two,
#[strum(serialize = "3")]
Three
// …
} // Also JokerCard
trait Card: Hash + Eq + Copy + Debug + Ord + FromStr {
fn is_joker(&self) -> bool;
}

Day 7
Card game
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, EnumString)]
enum StdCard {
#[strum(serialize = "2")]
Two,
#[strum(serialize = "3")]
Three
// …
} // Also JokerCard
impl Card for JokerCard {
fn is_joker(&self) -> bool {
matches!(self, Self::Joker)
}
}
trait Card: Hash + Eq + Copy + Debug + Ord + FromStr {
fn is_joker(&self) -> bool;
}
impl Card for StdCard {
fn is_joker(&self) -> bool {
false
}
}

Day 19
Parser
mod my_parser {
use pest_derive::Parser;
#[derive(Parser)]
#[grammar_inline = r#"
eoi = _{ !ANY }
cat = { "x" | "m" | "a" | "s" }
compare = { ("<" | ">") ~ ASCII_DIGIT+ }
ident = { ASCII_ALPHA_LOWER+ }
target = { ident | "A" | "R" }
single_rule = { cat ~ compare ~ ":" ~ target }
rules = { (single_rule ~ ",")+ }
line = { ident ~ "{" ~ rules ~ target ~ "}" }
file = { SOI ~ (line ~ NEWLINE*)* ~ eoi }
"#]
pub struct MyParser;
}
IDEs understand the generated code!

Day 22
3D block tower
fn compute1(text: &str) -> usize {
let mut blocks = read(text);
blocks.sort();
lower_blocks(&mut blocks);
removable_blocks(&blocks).len()
}
Setting up Blocks took ~100 lines (omitted!)
Rendered in Blender with Python

Day 22
3D block tower
fn compute1(text: &str) -> usize {
let mut blocks = read(text);
blocks.sort();
lower_blocks(&mut blocks);
removable_blocks(&blocks).len()
}
Setting up Blocks took ~100 lines (omitted!)
Rendered in Blender with Python

Day 22
3D block tower
fn compute2(text: &str) -> usize {
let mut blocks = read(text);
blocks.sort();
lower_blocks(&mut blocks);
blocks
.iter()
.map(|b| {
let mut new_blocks: Vec<Block> =
blocks.iter().filter(|x| *x != b).cloned().collect();
lower_blocks(&mut new_blocks);
blocks
.iter()
.filter(|x| *x != b)
.zip(new_blocks.iter())
.filter(|(x, y)| **x != **y)
.count()
})
.sum()
}

Day 24b
Worst Python - Rust comparison
from pathlib import Path
import sympy
def read(fn):
txt = Path(fn).read_text()
lines = [t.replace( "@", " ").split() for t in txt.splitlines()]
return [tuple(int(x.strip(",")) for x in a) for a in lines]
px, py, pz, dx, dy, dz = sympy.symbols( "px, py, pz, dx, dy, dz" , integer=True)
vals = read("24data.txt")
eqs = []
for pxi, pyi, pzi, dxi, dyi, dzi in vals[:3]:
eqs.append((pxi - px) * (dy - dyi) - (pyi - py) * (dx - dxi))
eqs.append((pyi - py) * (dz - dzi) - (pzi - pz) * (dy - dyi))
answer = sympy.solve(eqs)
print(answer)
This is the whole Python solution
Resorted to tricks to keep Rust solution manageable

Thoughts on Rust
Generally very positive!
•Loved the developer experience with Cargo
•Maxed out clippy (linter) with nursery lints and more
•Good support for functional programming was great
•Loved the Trait system
•Code wasn’t concise, but clear and fun to write
•Had to fight Rust occasionally, but code was better for it
•Had zero segfaults. Zero.

Thoughts on Rust
A few downsides
•Code was a lot longer than the Python version
•Code sometimes slower than the Python version (higher level libs)
•But found pretty good libs with ports of the Python algorithms
sometimes
•Much younger ecosystem than Python (but there _is_ one, unlike C++/C)
•Dependencies are “normal”, so can’t just use stdlib (good and bad)
•Some missing features, but it was easy to work around
•No generators or ternaries, for example

When would I use Rust?
Just some ideas
•For command line / developer tools
•Startup speed is unbelievable compared to any interpreter
•Options exist for unicode handling (unlike, say, C++…)
•For Python extensions (Python integration is top-notch)
•For things that don’t need CUDA
•When targeting WebAssembly
•If not needing libraries in other languages (though it has cross-compat)

Rust compared to other languages
A few, anyway
•You can write a library that depends on another library! (C++)
•Editions can work together (C++)
•No/little bad legacy code (like templates without constraints) (C++)
•Lazy functional, multiline closures, Traits, and explicit errors (Python)
•First-class support for features and profiles (C++ or Python)

Useful Links
adventofcode.com
blessed.rs
play.rust-lang.org
doc.rust-lang.org/book
henryiii.github.io/aoc2023
Photo by Chandler Cruttenden on Unsplash

More info?
Join the RSE Rust learning
group!

Bonus: Day 22
Python - Blender viz
import bpy
import bmesh
from mathutils import Matrix
import numpy as np
TXT = """\
1,0,1~1,2,1
0,0,2~2,0,2
0,2,3~2,2,3
0,0,4~0,2,4
2,0,5~2,2,5
0,1,6~2,1,6
1,1,8~1,1,9"""
def add_object(bm, start, end):
scale = np.abs(start - end) + 1
matrix = Matrix.LocRotScale((start + end) / 2, None, scale)
bmesh.ops.create_cube(bm, size=1.0, matrix=matrix)
bm = bmesh.new()
for line in TXT.splitlines():
ax, ay, az, bx, by, bz = map(float, line.replace("~", ",").split(","))
add_object(bm, np.array((ax, ay, az)), np.array((bx, by, bz)))

me = bpy.data.meshes.new( "Mesh")
bm.to_mesh(me)
bm.free()
obj = bpy.data.objects.new( "Object", me)
bpy.context.collection.objects.link(obj)