diff --git a/resources/blog-styles.css b/resources/blog-styles.css index db5acc1..f6c135c 100644 --- a/resources/blog-styles.css +++ b/resources/blog-styles.css @@ -64,6 +64,15 @@ white-space: nowrap; } +.blog-page--unhighlighted-block pre:not(.pygments-highlighter pre) { + /* Multi-line blocks */ + background-color: #f8f8f8; + + code { + white-space: pre; + } +} + .blog-page--has-toc.content-wrapper { width: 100%; display: grid; diff --git a/resources/default-styles.css b/resources/default-styles.css index 33599db..d66122e 100644 --- a/resources/default-styles.css +++ b/resources/default-styles.css @@ -9,7 +9,7 @@ width: 80%; margin-bottom: calc( var(--footer-elem-height) + 10px ); - p, li, blockquote { + p, li, blockquote, summary { font-size: 18px; } } diff --git a/sitemap.xml b/sitemap.xml index 7fccbfc..ef8ea5a 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -51,4 +51,7 @@ https://scherzer.dev/Blog/20250912-hello-world-rust + + https://scherzer.dev/Blog/20250919-rust-euler + diff --git a/src/Blog/posts/20250919-rust-euler.md b/src/Blog/posts/20250919-rust-euler.md new file mode 100644 index 0000000..8517992 --- /dev/null +++ b/src/Blog/posts/20250919-rust-euler.md @@ -0,0 +1,270 @@ +--- +extensions: + footnotes: true + pygments: true +extra-classes: + - blog-page--unhighlighted-block +title: Project Euler in Rust +--- + +# Project Euler in Rust + +After writing the obligatory "Hello, World!", I wanted to work on something +more substantial in Rust. When I learn a new programming language, my go-to +source of coding problems to try and solve in the new language is +[Project Euler][proj-euler], which features hundreds of coding challenges of +various difficulty levels. + +With Rust, I didn't try to solve any new problems, but rather focused on +problems that I had already solved in other languages. That way, I could be +focused on the specifics of working in Rust rather than generally figuring out +how to solve the problem. + +## Starter Problems + +I kicked things off with [Problem #1][proj-euler-prob-1], which asks for the +sum of all natural numbers below 1000 that are multiples of 3 or 5 (or both). +That problem is simple enough that I didn't refer back to any of my previous +solutions - my program[^1] was as simple as: + +```rust +fn main() { + let mut sum = 0; + for iii in 1..1000 { + if (iii % 3 == 0) || (iii % 5 == 0) { + sum += iii; + } + } + println!("Sum: {}", sum); +} +``` + +Working on this problem brought me to my first three major observations about +how Rust differs from other languages that I have used in the past: + +1. Rust variables are immutable by default. + +In other languages, immutability for local variables needs to be declared +explicitly (C, JavaScript) or is not possible (PHP). This will likely take some +getting used to, but I like it! + +2. The Rust compiler has helpful error messages. + +Without the `mut` keyword, trying to compile the program would lead to: + +
+ +Expand for errors + +``` +error[E0384]: cannot assign twice to immutable variable `sum` + --> problem1.rs:5:4 + | +2 | let sum = 0; + | --- first assignment to `sum` +... +5 | sum += iii; + | ^^^^^^^^^^ cannot assign twice to immutable variable + | +help: consider making this binding mutable + | +2 | let mut sum = 0; + | +++ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0384`. +``` +
+ +Not only does the compiler explain what went wrong (assigning twice to an +immutable variable), it also: + +* highlights were the assignments occur, +* suggests how to fix them, +* and offers a built-in tool for exploring the error code further + +That built-in tool explains that by default, variables in Rust are immutable. +Compared to the compiler errors that I am used to from C and C++, Rust is a +refreshing change of pace. + +3. The Rust compiler warns about style issues too. + +I'm used to the conditions for `if` statements needing to be surrounded with +`()`, i.e. `if ((iii % 3 == 0) || (iii % 5 == 0)) {`, but with Rust, those are +unnecessary. Not only that, but the compiler will warn that they are unused, and +suggest removing them. I appreciate the compiler pointing this out, though I'm +not sure how I feel about the compiler having style preferences. + +[Problem #2][proj-euler-prob-2], which asks for the sum of all even terms in +the Fibonacci sequence that are less than four million, was also pretty simple. +However, one of the compiler warnings that I got introduced me to a new Rust +feature: [`loop`][rust-loop], which is used for infinite loops. Other languages +that I have used lack a dedicated mechanism for infinite loops, and so I was +used to something like `while (true)`, but the compiler gave me a helpful +nudge via a warning. + +## Sudoku Solver + +Having gotten a hang of the basics, I figured it wouldn't hurt to just dive in, +and so I jumped to [Problem #96][proj-euler-prob-96]. Briefly, that problem +entails reading in 50 different sudoku grids, solving them, and then for each +grid, consider the first three digits of the top row as a three digit number, +and sum those three digit numbers across the 50 sudoku grids. + +To focus on learning Rust rather than re-solving the problem, I started off by +basically copying my C++ implementation of the problem and then working to adapt +it to Rust. I'm not going to discuss every difference between the languages, but +there were a few places where Rust was remarkably different from other +languages that I have used in the past that I think are worth mentioning for +anyone considering taking up Rust. + +The first major difference was a big pain point in my conversion of the C++ +code to Rust that required re-engineering my approach: + +4. Rust prohibits having multiple mutable references to the same value. + +Sudoku digits cannot repeat within the same row, column, or box of the grid. +To help with managing the nine rows, nine columns, and nine boxes, in C++ I had +a `DigitCollection` class that would hold pointers to the nine different +`Digit`s of the grid in a row/column/box. The `DigitCollection` could also +modify the values of the grid `Digit`s - if there was only one `Digit` that +could hold the value `1`, for example, then that `Digit` would be updated to be +a `1`. Then, the other two collections that the `Digit` was in would propagate +the change and remove the `1` option from the other `Digit`s. + +In Rust, such an approach was not going to work. I tried various workarounds +using Rust's standard library, like the [`std::cell::RefCell`][rust-refcell] +struct, but I wasn't able to get anything to work. As a result, I instead +refactored the way `DigitCollection` worked: instead of holding a reference to +the various `Digit` instances, they would hold the details of the *locations* +(row and column) of each `Digit` in the collection. Then, any time that the +`DigitCollection` wanted to do anything, it would fetch a reference to the +needed `Digit`. Those references were short-lived and scoped to their usage, so +there would not be multiple references to the same `Digit` at once as long as +only one `DigitCollection` was doing work at a time. + +At that point, I realized that the setup of the `DigitCollection` struct meant +that it was no longer tied to a specific grid. The `DigitCollection` only held +details of the *locations* of `Digit`s in the grid, not the `Digit`s themselves. +Thus, I should be able to allocate the 27 `DigitCollection` instances once at +the start (nine rows, nine columns, nine boxes) and just reuse those rather than +allocating 27 instances for each of the 50 grids. However, I quickly ran into +another limitation of Rust: + +5. Rust global variables have restrictions on their initialization. + +I was initializing the nine `DigitCollection` instances corresponding to the +rows of the grid with + +```rust +let row_sets: [DigitCollection; 9] = core::array::from_fn( + |row| + DigitCollection { + locations: core::array::from_fn(|col| DigitLoc{ row: row, col: col } ), + } +); +``` + +Each of the nine rows was a `DigitCollection`, where the locations were +initialized with a callback mapping the column index to a new `DigitLoc` +struct, which just holds the row and column information together.[^2] + +Unfortunately, while the snippet above worked fine within a function, doing it +for a global variable (which needs to be declared with either `const` or +`static` rather than `let`) did not work. Using either `static` or `const` +results in error E0015 complaining that the `std::array::from_fn` function is +non-const. + +The final big difference that I encountered was not an issue for the first few +grids, so I did not realize the impact right away: + +6. Rust does not have exceptions that can be thrown and caught. + +At various points in the solving process, the solver might conclude that the +grid is impossible to solve - for example, a `Digit` might not have any possible +values, or a value might not be an option in any `Digit` in a collection. At +that point, the C++ code would throw an exception; when converting to Rust, I +started by replacing those exceptions with Rust's [`panic!` macro][rust-panic]. + +However, some exceptions in the C++ version were meant to be caught and handled, +rather than terminating the entire program. Specifically, after implementing a +few basic solution stategies (e.g. checking if a `Digit` could only hold a +single value, or if a value could only go in one `Digit` in a `DigitCollection`) +my approach was rather inelegant: bifurcation. I would pick one `Digit` with +multiple options, set it to be one of those options, and then try to solve the +new grid. If solving succeeded, then the "guess" was correct and the solution +from the new grid was used. If solving failed, then the "guess" was incorrect +and that option could be removed from the `Digit` in question. + +To handle the "if solving failed" case, the C++ code would just catch an +exception; in Rust, the `panic!` macro aborts the entire program. I needed to +convert some of the `panic!` calls to instead use the [`Result`][rust-result] +type, which holds either a success value, or an error message. After that +migration, the program was able to compile, run, and produce the right answer! + +While I've spent a while on ways that Rust's differences were frustrating or +unexpected, there was also one Rust feature that the other languages I use most +often (C, PHP,[^3] and JavaScript) lack: + +7. Rust has pattern matching!!! + +I've used pattern matching in some other languages ([ML][ml-lang] comes to mind) +and it makes things very nice and compact. For example, part of the switch from +exceptions to a `Result` resulted in the code: + +```rust +match self.check_only_options() { + Err(_) => return false, + Ok(v) => made_change = v, +}; +``` + +If the `check_only_options()` method (which checks each `DigitCollection` to see +if there is a value that can only go into one `Digit`) returns an error, +indicating that the solving is impossible (due to bifurcation), the solving +just returns false. + +## Final Thoughts + +Rust is definitely a language I want to keep using. I plan to continue playing +around with Rust for Project Euler problems, and I might try to contribute +[more patches to mago][blog-mago]. While Rust will take some adjusting to, +especially the limits on mutable references, I look forward to adding a new +language to my repertoire. + +I also need to get familiar with Rust's package system for installing libraries, +[crates][rust-crates]. These seem to be a built-in part of the core language, +unlike [PHP's Composer packages][php-composer] or +[JavaScript's NPM packages][js-npm], where the package management tools are +provided by third parties. + +[^1]: Normally, publishing solutions to the Project Euler problems is +discouraged, but for the first 100 problems sharing solutions is allowed. + +[^2]: In Sudoku, the cells are labeled as row one (top) through row nine +(bottom) and column one (left) through column nine (right). However, in Rust and +most other programming languages that I have used, arrays are indexed starting +from `0`. Thus, the "location" of a `Digit`, indicating where it is in the +two-dimensional array of `Digits` in the grid, has a row and a column that are +in the range 0-8 rather than 1-9. + +[^3]: PHP might be getting pattern matching at some point, there is +[an RFC][php-pattern-rfc] currently in draft to add that feature, but the RFC +has been in draft since 2020 so it could still be a while before it gets +discussed, accepted, and implemented, if it ever does. + +[proj-euler]: https://projecteuler.net/ +[proj-euler-prob-1]: https://projecteuler.net/problem=1 +[proj-euler-prob-2]: https://projecteuler.net/problem=2 +[rust-loop]: https://doc.rust-lang.org/rust-by-example/flow_control/loop.html +[proj-euler-prob-96]: https://projecteuler.net/problem=96 +[rust-refcell]: https://doc.rust-lang.org/std/cell/struct.RefCell.html +[rust-panic]: https://doc.rust-lang.org/std/macro.panic.html +[rust-result]: https://doc.rust-lang.org/std/result/ +[php-pattern-rfc]: https://wiki.php.net/rfc/pattern-matching +[ml-lang]: https://en.wikipedia.org/wiki/ML_(programming_language) +[blog-mago]: ./20250906-mago-rust +[rust-crates]: https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html +[php-composer]: https://getcomposer.org/doc/00-intro.md +[js-npm]: https://docs.npmjs.com/about-npm \ No newline at end of file diff --git a/tests/data/Home.html b/tests/data/Home.html index 184c292..ad20b18 100644 --- a/tests/data/Home.html +++ b/tests/data/Home.html @@ -4,5 +4,8 @@ Computer Science and Political Science and graduating magna cum laude in 2024 with a Bachelor of Science degree. As part of my work in Computer Science, I chose to write an honors thesis in my senior year, see here for details. I continued at Tufts for graduate school, graduating in 2025 with a Master of Science in Computer Science degree.

See the links in the navigation bar above for more information about my -experience.

Contact

Blog

I also have a blog. You can see a full index of my posts here. My latest blog post is:

"Hello, World!" in Rust

Friday, 12 September 2025

Yesterday, I started to teach myself how to program in Rust. As always, the first -step was a "Hello, World!" program. Continue reading...

\ No newline at end of file +experience.

Contact

Blog

I also have a blog. You can see a full index of my posts here. My latest blog post is:

Project Euler in Rust

Friday, 19 September 2025

After writing the obligatory "Hello, World!", I wanted to work on something +more substantial in Rust. When I learn a new programming language, my go-to +source of coding problems to try and solve in the new language is +Project Euler, which features hundreds of coding challenges of +various difficulty levels. Continue reading...

\ No newline at end of file diff --git a/tests/data/blog-index.html b/tests/data/blog-index.html index ddd589d..3b612ff 100644 --- a/tests/data/blog-index.html +++ b/tests/data/blog-index.html @@ -1,5 +1,9 @@ -Blog index
HomeRésuméOpen SourceWorkBlog

Blog index

"Hello, World!" in Rust

Friday, 12 September 2025

Yesterday, I started to teach myself how to program in Rust. As always, the first +Blog index

Blog index

Project Euler in Rust

Friday, 19 September 2025

After writing the obligatory "Hello, World!", I wanted to work on something +more substantial in Rust. When I learn a new programming language, my go-to +source of coding problems to try and solve in the new language is +Project Euler, which features hundreds of coding challenges of +various difficulty levels. Continue reading...

"Hello, World!" in Rust

Friday, 12 September 2025

Yesterday, I started to teach myself how to program in Rust. As always, the first step was a "Hello, World!" program. Continue reading...

First Rust Patch

Saturday, 06 September 2025

As part of preparing for my Longhorn PHP talk about what is new in PHP 8.5, I have been going through various PHP tools and checking to what extent they support the new features. When the support is lacking, I have sometimes tried to