├── .gitignore ├── main.rs ├── posts ├── onivim2 │ ├── assets │ │ ├── onivim.png │ │ └── license_timeline.jpg │ └── index.md ├── tco-story │ ├── assets │ │ ├── pseudocode.jpg │ │ ├── mailing-list.jpg │ │ ├── jonhoo-comment.png │ │ └── timthelion-comment.png │ └── index.md ├── lru-cache │ ├── assets │ │ ├── sea-of-objects.png │ │ ├── array-backed-dll.png │ │ └── python-impl-layout.png │ └── index.md ├── rust-representer │ ├── assets │ │ └── visit_mut_methods.png │ └── index.md ├── haskell-from-rust-I │ └── index.md ├── takeaways-I │ └── index.md ├── semaphores │ └── index.md └── extending-minigrep │ └── index.md ├── license-mit ├── README.md └── license-apache /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("GitHub, please treat this repo as a Rust repo 🦀"); 3 | } 4 | -------------------------------------------------------------------------------- /posts/onivim2/assets/onivim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/onivim2/assets/onivim.png -------------------------------------------------------------------------------- /posts/tco-story/assets/pseudocode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/tco-story/assets/pseudocode.jpg -------------------------------------------------------------------------------- /posts/tco-story/assets/mailing-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/tco-story/assets/mailing-list.jpg -------------------------------------------------------------------------------- /posts/lru-cache/assets/sea-of-objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/lru-cache/assets/sea-of-objects.png -------------------------------------------------------------------------------- /posts/onivim2/assets/license_timeline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/onivim2/assets/license_timeline.jpg -------------------------------------------------------------------------------- /posts/tco-story/assets/jonhoo-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/tco-story/assets/jonhoo-comment.png -------------------------------------------------------------------------------- /posts/lru-cache/assets/array-backed-dll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/lru-cache/assets/array-backed-dll.png -------------------------------------------------------------------------------- /posts/lru-cache/assets/python-impl-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/lru-cache/assets/python-impl-layout.png -------------------------------------------------------------------------------- /posts/tco-story/assets/timthelion-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/tco-story/assets/timthelion-comment.png -------------------------------------------------------------------------------- /posts/rust-representer/assets/visit_mut_methods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanchen1991/blog/HEAD/posts/rust-representer/assets/visit_mut_methods.png -------------------------------------------------------------------------------- /license-mit: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sean Chen's Blog 🦀 2 | 3 | Blog posts, mostly about Rust. 4 | 5 | ## Posts 6 | 7 | | Date | Title | 8 | |-|-| 9 | | 2021-04-06 | [A Beginner's Guide to Handling Errors in Rust](./posts/extending-minigrep/index.md) | 10 | | 2021-01-23 | [Implementing an LRU Cache in Rust](./posts/lru-cache/index.md) | 11 | | 2020-11-27 | [Semaphores in Rust](./posts/semaphores/index.md) | 12 | | 2020-08-18 | [Why I'm Switching from VSCode to OniVim 2](./posts/onivim2/index.md) | 13 | | 2020-07-20 | [Haskell::From(Rust) I: Infix Notation and Currying](./posts/haskell-from-rust-I/index.md) | 14 | | 2020-07-13 | [Some Learnings from Implementing a Normalizing Rust Representer](./posts/rust-representer/index.md) | 15 | | 2020-06-03 | [The Story of Tail Call Optimizations in Rust](./posts/tco-story/index.md) | 16 | | 2020-05-27 | [Takeaways from My Initial Exposure to Rust](./posts/takeaways-I/index.md) | 17 | 18 | ## Notifications 19 | 20 | Get notified when a new post gets published by watching this repo's releases (click `Watch` -> click `Custom` -> select `Releases` -> click `Apply`). 21 | 22 | ## Feedback 23 | 24 | If you have any feedback, please open an [issue][issue] on this repo. PRs for even minor fixes like typos and grammar are appreciated! 25 | 26 | ## Discuss 27 | 28 | Feel free to start a discussion on any of the content published in this blog over in the [discussions forum](https://github.com/seanchen1991/blog/discussions)! 29 | 30 | ## Licensing 31 | 32 | To be compatible with [Rust](https://github.com/rust-lang/rust), all code examples in this blog are licensed under [Apache License Version 2.0](./license-apache) or [MIT License](./license-mit), at your option. 33 | 34 | I'd like to retain exclusive rights to the English version of the posts themselves, but as mentioned above if you translate a post into another language you're welcome to promote your translations however you like. 35 | 36 | [issue]: https://github.com/seanchen1991/blog/issues/new 37 | [Rust]: https://github.com/rust-lang/rust 38 | [license-apache]: ./license-apache 39 | [license-mit]: ./license-mit 40 | -------------------------------------------------------------------------------- /posts/haskell-from-rust-I/index.md: -------------------------------------------------------------------------------- 1 | # Haskell::From(Rust) I: Infix Notation and Currying 2 | _July 20th, 2020 | #haskell | #rust | #exercism_ 3 | 4 | ## Prelude 5 | 6 | I’ve been meaning to learn Haskell for a while now. Most people who write Rust likely have had at least a background-level exposure to Haskell (if they hadn’t already encountered/learned the language on their own prior). Since the language (along with ML and possibly OCaml?) had such an effect on Rust’s development, digging into it will likely pay dividends in the form of improving my understanding of Rust. 7 | 8 | This _Haskell::From(Rust)_ series will chronicle some of the learnings I glean from learning Haskell, as well as the takeaways that can be applied to write better code in Rust. 9 | 10 | ## The Setup 11 | 12 | While working through Exercism’s Haskell track, specifically the problem asking you to implement a [pangram](https://en.wikipedia.org/wiki/Pangram) checker, I encountered a confusion point regarding [infix functions](https://wuciawe.github.io/functional%20programming/haskell/2016/07/03/infix-functions-in-haskell.html). From what I can gather, this is not an uncommon sticking point for those who are new to Haskell. 13 | 14 | In order to solve this pangram problem, I opted to sort the input string and filter out any duplicates: 15 | 16 | ```haskell 17 | isPangram text = [‘a’..’z’] == (List.nub . List.filter (/=‘ ‘) . List.sort $ text) 18 | ``` 19 | 20 | Afterwards, I browsed some others’ implementations and came across this one: 21 | 22 | ```haskell 23 | isPangram text = all (`elem` lowercased) [‘a’..’z’] 24 | where lowercased = map toLower text 25 | ``` 26 | 27 | ## The Turn 28 | 29 | I read ``(`elem` lowercased) [‘a’..’z’]`` as checking that the first element in the `lowercased` string is a member of the set `[‘a’..’z’]`. The `all` function then facilitates iterating through all of the characters of `lowercased`, ensuring that no iterations return false. 30 | 31 | In short, I thought this line checked that every character in the input string was a lower-cased letter. But that shouldn’t be sufficient for implementing a pangram function. Upon testing out the code, it did indeed work (passing it an input string that didn’t contain all of the English letters caused it to return false), so clearly there was a hole in my understanding of this logic. 32 | 33 | ## The Prestige 34 | 35 | After doing some research and posting my question on Haskell forums, I found an explanation that clicked: 36 | 37 | > Any operator (including `foo`) can be written in prefix notation by surrounding it with parentheses: `(+)`, `(==)`, ``(`div`)``. These are called _sections_, and they’re regular functions like `map` and `sin`. For example, instead of `2 + 2`, you can write `(+) 2 2`. You can even leave off either the left or right operand to curry an operator. For example, ``(`div` 0)`` is a function that takes one argument and divides it by zero. 38 | 39 | - _Haskell: The Confusing Parts_ [1](http://echo.rsmw.net/n00bfaq.html) 40 | 41 | The last sentence is what made it click for me: this was an example of [currying](https://en.wikipedia.org/wiki/Currying). Essentially, ``(`elem` lowercased)`` leveraged currying to define a new function that takes one argument and checks that that argument is an element of `lowercased`. My understanding had been flipped based off of how I read the function, which was heavily influenced by what programming languages I’d had prior experience with. 42 | 43 | Clearly, if one comes from an imperative or OOP background, this sort of syntax can be strange. Even Rust, with its relatively strong emphasis on functional norms (especially when it comes to chaining iterator adapters), isn’t nearly as expressive when it comes to the number of ways in which functions can be defined in comparison to Haskell. 44 | 45 | This is, however, a feature of Haskell that I find quite enticing. I’m looking forward to learning more and digging in deeper! 46 | 47 | ## Before We Part Ways 48 | 49 | I couldn’t leave my dear readers without at least a bit of Rust code. That’d be inexcusable! 50 | 51 | Using the same pieces present in the Haskell implementation, we can implement a pangram function in Rust like this: 52 | 53 | ```rust 54 | fn is_pangram(text: &str) -> bool { 55 | maps to `[‘a’..’z’]` 56 | let alphabet = “abcdefghijklmnopqrstuvwxyz”; 57 | 58 | // maps to `where lowercased = map toLower text` 59 | let lowercased = text.to_lowercase(); 60 | 61 | // maps to `all (`elem` lowercased)` 62 | alphabet.chars().all(|letter| lowercased.contains(letter)) 63 | } 64 | ``` 65 | 66 | Not as elegant as the Haskell version, but also not too far off in my opinion. 67 | 68 | **_Edit_** 69 | 70 | A reader, Maxwell Orok, reached out to me to let me know that char ranges are a thing in Rust now as of version [1.45](https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html). This is great, as it allows us to clean up the Rust implementation a bit: 71 | 72 | ```rust 73 | fn is_pangram(text: &str) -> bool { 74 | // maps to `[‘a’..’z’]` 75 | // We can now collect into a `String` from a `Range` 76 | // instead of having to hard code the string 77 | let alphabet: String = ('a'..='z').collect(); 78 | 79 | // maps to `where lowercased = map toLower text` 80 | let lowercased = text.to_lowercase(); 81 | 82 | // maps to `all (`elem` lowercased)` 83 | alphabet.chars().all(|letter| lowercased.contains(letter)) 84 | } 85 | ``` 86 | 87 | Definitely much better in my opinion! 88 | -------------------------------------------------------------------------------- /posts/takeaways-I/index.md: -------------------------------------------------------------------------------- 1 | # Takeaways from My Initial Exposure to Rust 2 | _May 27th, 2020 | #rust | #introspection_ 3 | 4 | My journey into learning Rust and getting into its wonderful open source community has been, on the whole, pretty stop-and-go. 5 | 6 | ## First Encounter 7 | 8 | I was introduced to Rust way back in 2014, probably around the time when it was around version 0.8 or so. I was a student in Hack Reactor’s sixteenth cohort, learning JavaScript, Angular, and Node.js in preparation for taking on a role as a web developer. One of the TAs (or Hackers-In-Residence as they were called back then) who mentored my cohort was an early adopter of Rust and was very active in the open source community. 9 | 10 | He was (and still is?) a rather ardent advocate for Rust, citing its algebraic data types and its functional idioms. While my memories of that time are rather hazy, one thing I can say with absolute certainty is that I had zero idea of what any of it meant; I just nodded my head and pretended I understood. 11 | 12 | To his credit, this TA, perhaps noticing my wide-eyed curiosity, mentored me in writing a tiny Rust library that I published on my then-fledgling GitHub account. I had very little idea what any of the code he helped me write meant, though it was apparently a useful utility for him that he pulled in as a dependency on whatever crazy project he was working on at the time. 13 | 14 | ## First Public Rust Code 15 | 16 | Here’s the code for that early project, in all of its glorious version 0.8 Rust goodness: 17 | 18 | ```rust 19 | //! A reader + writer stream backed by an in-memory buffer. 20 | 21 | use std::io; 22 | use std::slice; 23 | use std::cmp::min; 24 | use std::io::IoResult; 25 | 26 | /// `MemStream` is a reader + writer stream backed by an in-memory buffer 27 | pub struct MemStream { 28 | buf: Vec, 29 | pos: uint 30 | } 31 | 32 | #[deriving(PartialOrd)] 33 | impl MemStream { 34 | /// Creates a new `MemStream` which can be read and written to 35 | pub fn new() -> MemStream { 36 | MemStream { 37 | buf: vec![], 38 | pos: 0 39 | } 40 | } 41 | /// Tests whether this stream has read all bytes in its ring buffer 42 | /// If `true`, then this will no longer return bytes from `read` 43 | pub fn eof(&self) -> bool { self.pos >= self.buf.len() } 44 | /// Acquires an immutable reference to the underlying buffer of 45 | /// this `MemStream` 46 | pub fn as_slice<'a>(&'a self) -> &'a [u8] { self.buf.as_slice() } 47 | /// Unwraps this `MemStream`, returning the underlying buffer 48 | pub fn unwrap(self) -> Vec { self.buf } 49 | } 50 | 51 | impl Reader for MemStream { 52 | fn read(&mut self, buf: &mut [u8]) -> IoResult { 53 | if self.eof() { return Err(io::standard_error(io::EndOfFile)) } 54 | let write_len = min(buf.len(), self.buf.len() - self.pos); 55 | { 56 | let input = self.buf.slice(self.pos, self.pos + write_len); 57 | let output = buf.slice_mut(0, write_len); 58 | assert_eq!(input.len(), output.len()); 59 | slice::bytes::copy_memory(output, input); 60 | } 61 | self.pos += write_len; 62 | assert!(self.pos <= self.buf.len()); 63 | 64 | return Ok(write_len); 65 | } 66 | } 67 | 68 | impl Writer for MemStream { 69 | fn write(&mut self, buf: &[u8]) -> IoResult<()> { 70 | self.buf.push_all(buf); 71 | Ok(()) 72 | } 73 | } 74 | ``` 75 | 76 | It’s essentially a wrapper around a `Vec` that implements the `std::io::Reader` and `std::io::Writer` traits. `Reader`s are objects that can be read from, while `Writer`s are objects that can be written to. Files would be one prime example of something that requires these traits. 77 | 78 | > Note: Since those early pre-1.0 days, the trait names have been shorted from `Reader` and `Writer` to just `Read` and `Write`. Evidently, back in 0.8, the `Write` trait only required a `write` method to fulfill the trait. Nowadays, a `flush` method is also required as a way to completely drain all of the contents of the `Write`r. 79 | 80 | Looking back at it now, it’s pretty cool to see that, even in its pre-1.0 days, Rust code really doesn’t look all that different from its current incarnation. 81 | 82 | Context is important here. I was not a seasoned or confident programmer at the time by any means. I’d been learning how to assemble simple web front ends and back-end APIs for the past few months. I most certainly couldn’t tell you the difference between the stack and the heap; apparently it just wasn’t necessary to know about in order to implement web applications. I was missing so much foundational knowledge and background that I had no inkling why Rust’s philosophy and its combination of features were (and are) game-changing for the wider programming community. 83 | 84 | To be honest, I think I just wanted some of this TA’s genius to rub off on me. 85 | 86 | ## Goodbye For Now 87 | 88 | The months following Hack Reactor were a whirlwind of interviews and first-job jitters that consumed my waking days. I wouldn’t revisit Rust for almost two years, but the seed had been planted. 89 | 90 | Timing can be everything when it comes to learning. At the outset of my programming journey, I was certainly too immature to understand Rust. That being said, I also think that due to the Rust seed having been planted in my mind early on, my subsequent learnings were shaped by that early exposure. 91 | 92 | Indeed I would come to find out in the next few years that I really did not find web development all that interesting. I was drawn much more to concepts and ideas belonging to the realms of operating systems, systems programming, basically “lower level stuff”. To be fair, perhaps I was interested in these sorts of things just 'cause, and not actually because I was exposed to a low-level systems programming language early on. But the end result was that I naturally found my way back to Rust, and this time, much better equipped to really dig into the language and start learning it in earnest. 93 | -------------------------------------------------------------------------------- /posts/onivim2/index.md: -------------------------------------------------------------------------------- 1 | # Why I'm Switching from VSCode to OniVim 2 2 | _August 18th, 2020 | #open_source_ 3 | 4 | As someone interested in open source sustainability, I recently heard about a very interesting open source licensing scheme that a project called [OniVim 2](https://github.com/onivim/oni2) uses. The central premise of the project itself is to combine the efficient modal editing functionality of Vim with a modern text editor UI. 5 | 6 | ![OniVim's design is clearly inspired by VSCode](assets/onivim.png) 7 | 8 | ## How OniVim’s Licensing Scheme Works 9 | 10 | The OniVim 2 editor is currently under active development by [Outrun Labs](https://www.outrunlabs.com/). Any updates pushed to the project by them go under a commercial license; this is the closed source half of the scheme. Conversely, any updates merged into the project from outside Outrun go under an MIT license; this is the open source half of the scheme. Lastly, any code licensed under the commercial license is transferred over to the MIT license after an 18 month window. 11 | 12 | Anyone can freely use the 18-month-old version of the project. For those who wish use the up-to-date version, a commercial license must be purchased. 13 | 14 | ![A timeline of OniVim's licensing scheme](assets/license_timeline.jpg) 15 | 16 | You can read more about the why and how of this scheme [here](https://onivim.github.io/docs/other/motivation#a-new-model-for-sustainability). 17 | 18 | ## The Vim Editor I Never Knew I Needed 19 | 20 | I’ve been trying to embrace Vim for years, but have, up to this point, been mostly unsuccessful. I’ve internalized the fundamental Vim keybindings, but have never gotten much further than that. I would always become overwhelmed with the sheer amount of freedom and choice when it came to how much you could customize your Vim experience through plugins and other customizations. 21 | 22 | Certainly there are people who love the level of customizability that Vim provides. I prefer something sensible out-of-the-box that doesn’t require ~~any~~ much fiddling, with the option for configurability should the need arise (this is probably why I'll likely never migrate off of OSX). 23 | 24 | For a long time I defaulted to VSCode with the Vim keybindings plugin enabled. However, this always felt like a subpar compromise; VSCode is clearly not built with Vim keybindings in mind. 25 | 26 | The first time I fired up an instance of OniVim 2, I was blown away by the snappiness of it. That, and the fact that it is very clearly designed around the modal editing experience, prompted me to shell out for a commercial license right then and there. 27 | 28 | OniVim 2 is a “batteries included” editor from the get-go, but one of its most powerful extensibility features is the fact that it allows users to hook into the VSCode plugin ecosystem. As a result, installing and enabling useful extensions will be just as easy with OniVim 2 as it is with VSCode. 29 | 30 | There’s certainly a lot of potential here, but at the same time, keep in mind that the project is still in an alpha stage. There are many key features still missing at this point, such as an autosave feature (so you have to keep typing `:w` every so often), as well as being able to pull in your `.vimrc` file for further customization (this is super important as it will enable a more seamless transition for vim/neovim users). I also ran into an [issue](https://github.com/onivim/oni2/issues/2307#issuecomment-675731640) where a missing extension file needed for Haskell syntax highlighting caused the entire program to crash. 31 | 32 | Despite these issues, I’m very much looking forward to following along with OniVim’s development as time goes on and seeing the incremental improvements to the overall editing experience! 33 | 34 | ## Some Reasons Why Open Source Isn’t Sustainable 35 | 36 | In addition to having the potential to become an editor that I can see myself adopting as my go-to, I’m also really interested in seeing how things shake out for OniVim on the sustainability front. 37 | 38 | Nadia Eghbal, in her new book [_Working in Public_](https://www.goodreads.com/book/show/54140556-working-in-public), argues that the widespread notion that the main problems open source communities face is a lack of contributors and/or a lack of funding don’t reveal the whole scope of the problem. 39 | 40 | She makes the point that in recent years, contributing to open source projects has become very frictionless, due in large part to the proliferation and innovations of GitHub. But this ends up resulting in very high _turnover rate_ for open source projects. 41 | 42 | Maintainers end up having to spend a lot of time interfacing with the revolving door of casual contributors looking to submit a PR or two to their project. Ultimately, this is work that most maintainers do not want to be doing in their spare time, which is a recipe for maintainer burnout. 43 | 44 | ## How OniVim’s Licensing Scheme (Partly) Addresses These Issues 45 | 46 | One possible way to address the unsustainable nature of open source, according to Nadia in her book, is to make it _harder_ for people to contribute to open source projects. The idea being that _committed_ users and contributors are more invested in a project, which often leads to higher-quality contributions. 47 | 48 | The really cool thing about OniVim’s licensing scheme is the fact that it kills two birds with one stone. On the one hand, charging for commercial licenses means the project has some funding that benefits the maintainers. One the other hand, it also provides a way to filter for the more _committed_ users! 49 | 50 | The maintainers of OniVim mention [here](https://onivim.github.io/docs/other/motivation#focusing-on-the-right-priorities) that, as the project attracts more users, issues will be prioritized based on whether it was logged by a user who owns a commercial license. This means they’ll be spending less time triaging and responding to issues, and more time iterating on the project itself. 51 | 52 | Some open questions in my mind include: 53 | 54 | Will it lead to a more sustainable workflow for the maintainers down the line? Will prioritizing the issues raised by paying users lead the project down a good direction? Will the financials and the economics end up working out? 55 | 56 | With all that being said, I’m really curious to see how this licensing scheme works out for the OniVim maintainers long term! 57 | -------------------------------------------------------------------------------- /posts/semaphores/index.md: -------------------------------------------------------------------------------- 1 | # Semaphores in Rust 2 | _November 27th, 2020 | #rust | #synchronization_primitives | #the_little_book_of_semaphores | #intermediate_ 3 | 4 | > Note: I recently stumbled upon Allen Downey’s [_The Little Book of Semaphores_](https://greenteapress.com/wp/semaphores), which is a short, sweet, and to-the-point textbook on the topic of synchronization. 5 | > Naturally, reading through the book inspired me to implement these synchronization primitives and problems in Rust. 6 | > I hope others find this exploration interesting, insightful, and/or helpful! 🙂 7 | 8 | Implementing synchronization primitives in Rust in a safe fashion is a bit strange in a circular kind of way. It’s similar to implementing a hash map in JavaScript: since everything in JS boils down to an Object, you’re essentially implementing a hash map using hash maps! 9 | 10 | Similarly, for this semaphore implementation we’ll be using standard library types that are “more complex” from a conceptual and implementation standpoint than the primitive we’re actually implementing. 11 | 12 | I’m sure there’s a more low-level way to do this in Rust that better captures the primitive nature of these primitives, but that likely involves writing unsafe code and I’m just not comfortable enough yet with that. Maybe in the future, this will be something I’ll revisit! 13 | 14 | ## What are Semaphores? 15 | 16 | Semaphores are one of the simplest synchronization primitives upon which more complex tools (such as mutexes, barriers, etc.) can be built upon. 17 | 18 | Conceptually, a semaphore is just an integer that can only be incremented and decremented in an atomic fashion. You can imagine that the integer represents some number of resources that only a certain number of threads are able to access or acquire at the same time. 19 | 20 | > Note: The term _atomic_ means that even when multiple threads are all attempting to change the value of the semaphore, those changes occur in a sequential (and thus deterministic) order. 21 | 22 | There are different implications depending on whether the integer is in a positive or negative state. If it’s in a positive state `n`, this means that `n` threads are able to access the protected resource. If it’s in a negative state `-n`, this means that `n` threads are all blocked and waiting for access to the protected resource. 23 | 24 | ## A Basic Semaphore Implementation 25 | 26 | From the above description, our semaphore will contain an integer that can only be access in an atomic fashion. It will need a method that allows a thread to access the underlying critical section and increment the atomic integer in the process. It will also need a method that allows a thread to decrement the atomic integer and signal a waiting thread (assuming there is at least one) that they may acquire the semaphore. 27 | 28 | This is where what I said earlier about using more “advanced” synchronization primitives to implement a simpler one comes into play. In order to notify one of the threads waiting on the semaphore, we’ll need to make use of a [condition variable][condvar], which is the tool that facilitates threads waiting for their turn. It’s also what allows the semaphore to notify a waiting thread that they may not acquire the semaphore. 29 | 30 | In conjunction with a condition variable, we’ll also be using a `Mutex` to ensure that our integer is only ever accessed by one thread at a time. 31 | 32 | ```rust 33 | use std::sync::{Mutex, Condvar}; 34 | 35 | pub struct Semaphore { 36 | condvar: Condvar::new(), 37 | counter: Mutex 38 | } 39 | ``` 40 | 41 | Now let’s add our two methods. We’ll call the method that decrements the semaphore `acquire` to communicate the fact that when a thread calls this method, it’s acquiring one of the protected resources: 42 | 43 | ```rust 44 | impl Semaphore { 45 | pub fn acquire(&self) { 46 | // gain access to the atomic integer 47 | let mut count = self.counter.lock().unwrap(); 48 | 49 | // wait so long as the value of the integer <= 0 50 | while *count <= 0 { 51 | count = self.condvar.wait(count).unwrap(); 52 | } 53 | 54 | // decrement our count to indicate that we acquired 55 | // one of the resources 56 | *count -= 1; 57 | } 58 | } 59 | ``` 60 | 61 | We’ll call the second method that increments the semaphore `release` to indicate the fact that it’s releasing one of the protected resources so that another waiting thread may access it: 62 | 63 | ```rust 64 | impl Semaphore { 65 | ... 66 | pub fn release(&self) { 67 | // gain access to the atomic integer 68 | let mut count = self.counter.lock().unwrap(); 69 | 70 | // increment its value 71 | *count += 1; 72 | 73 | // notify one of the waiting threads 74 | self.condvar.notify_one(); 75 | } 76 | } 77 | ``` 78 | 79 | And that should do it for our (relatively) basic semaphore implementation! 80 | 81 | Let’s go ahead and wrap this up by adding a few tests: 82 | 83 | ```rust 84 | #[cfg(test)] 85 | mod tests { 86 | use super::Semaphore; 87 | use std::thread; 88 | use std::sync::Arc; 89 | use std::sync::mpsc::channel; 90 | 91 | #[test] 92 | fn test_sem_acquire_release() { 93 | let sem = Semaphore::new(1); 94 | 95 | sem.acquire(); 96 | sem.release(); 97 | sem.acquire(); 98 | } 99 | 100 | #[test] 101 | fn test_child_waits_parent_signals() { 102 | let s1 = Arc::new(Semaphore::new(0)); 103 | let s2 = s1.clone(); 104 | 105 | let (tx, rx) = channel(); 106 | 107 | let _t = thread::spawn(move || { 108 | s2.acquire(); 109 | tx.send(()).unwrap(); 110 | }); 111 | 112 | s1.release(); 113 | let _ = rx.recv(); 114 | } 115 | 116 | #[test] 117 | fn test_parent_waits_child_signals() { 118 | let s1 = Arc::new(Semaphore::new(0)); 119 | let s2 = s1.clone(); 120 | 121 | let (tx, rx) = channel(); 122 | 123 | let _t = thread::spawn(move || { 124 | s2.release(); 125 | let _ = rx.recv(); 126 | }); 127 | 128 | s1.acquire(); 129 | tx.send(()).unwrap(); 130 | } 131 | } 132 | ``` 133 | 134 | In the next post we’ll be using our homebrew semaphore to implement some other synchronization primitives! 135 | 136 | [condvar]: https://doc.rust-lang.org/std/sync/struct.Condvar.html 137 | -------------------------------------------------------------------------------- /license-apache: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /posts/tco-story/index.md: -------------------------------------------------------------------------------- 1 | # The Story of Tail Call Optimizations in Rust 2 | _June 3rd, 2020 | #rust | #open_source_ 3 | 4 | I think tail call optimizations are pretty neat, particularly how they work to solve a fundamental issue with how recursive function calls execute. Functional languages like Haskell and those of the Lisp family, as well as logic languages (of which Prolog is probably the most well-known exemplar) emphasize recursive ways of thinking about problems. These languages have much to gain performance-wise by taking advantage of tail call optimizations. 5 | 6 | > Note: I won't be describing what tail calls are in this post. Here are a number of good resources to refer to: 7 | > - The YouTube channel Computerphile has a [video](https://youtu.be/_JtPhF8MshA) where they walk through examples of tail-recursive functions in painstaking detail. 8 | > - A detailed explanation on [Stack Overflow](https://stackoverflow.com/questions/310974/what-is-tail-call-optimization) on the concept. 9 | 10 | With the recent trend over the last few years of emphasizing functional paradigms and idioms in the programming community, you would think that tail call optimizations show up in many compiler/interpreter implementations. And yet, it turns out that many of these popular languages _don’t_ implement tail call optimization. JavaScript had it up till a few years ago, when it removed support for it [1](https://stackoverflow.com/questions/42788139/es6-tail-recursion-optimisation-stack-overflow). Python doesn’t support it [2](http://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html). Neither does Rust. 11 | 12 | Before we dig into the story of why that is the case, let’s briefly summarize the idea behind tail call optimizations. 13 | 14 | ## How Tail Call Optimizations Work (In Theory) 15 | 16 | Tail-recursive functions, if run in an environment that doesn’t support TCO, exhibits linear memory growth relative to the function’s input size. This is because each recursive call allocates an additional stack frame to the call stack. The goal of TCO is to eliminate this linear memory usage by running tail-recursive functions in such a way that a new stack frame doesn’t need to be allocated for each call. 17 | 18 | One way to achieve this is to have the compiler, once it realizes it needs to perform TCO, transform the tail-recursive function execution to use an iterative loop. This means that the result of the tail-recursive function is calculated using just a single stack frame. Ta-da! Constant memory usage. 19 | 20 | ![Drawing Pseudocode](assets/pseudocode.jpg) 21 | 22 | With that, let’s get back to the question of why Rust doesn’t exhibit TCO. 23 | 24 | ## Going Through the Rust Wayback Machine 25 | 26 | The earliest references to tail call optimizations in Rust I could dig up go all the way back to the Rust project’s inception. I found [this][mailing_list] mailing list thread from 2013, where Graydon Hoare enumerates his points for why he didn’t think tail call optimizations belonged in Rust: 27 | 28 | ![Mailing list](assets/mailing-list.jpg) 29 | 30 | That mailing list thread refers to [this][gh_issue] GitHub issue, circa 2011, when the initial authors of the project were grappling with how to implement TCO in the then-budding compiler. The heart of the problem seemed to be due to incompatibilities with LLVM at the time; to be fair, a lot of what they’re talking about in the issue goes over my head. 31 | 32 | What I find so interesting though is that, despite this initial grim prognosis that TCO wouldn’t be implemented in Rust (from the original authors too, no doubt), people to this day still haven’t stopped trying to make TCO a thing in rustc. 33 | 34 | ## Subsequent Proposals for Adding TCO into Rustc 35 | 36 | In May of 2014, [this][pr_81] PR was opened, citing that LLVM was now able to support TCO in response to the earlier mailing list thread. More specifically, this PR sought to enable on-demand TCO by introducing a new keyword `become`, which would prompt the compiler to perform TCO on the specified tail recursive function execution. 37 | 38 | Over the course of the PR’s lifetime, it was pointed out that rustc could, in certain situations, infer when TCO was appropriate and perform it [3](https://github.com/rust-lang/rfcs/issues/271#issuecomment-271161622). The proposed `become` keyword would thus be similar in spirit to the `unsafe` keyword, but specifically for TCO. 39 | 40 | A subsequent [RFC][rfc] was opened in February of 2017, very much in the same vein as the previous proposal. Interestingly, the author notes that some of the biggest hurdles to getting tail call optimizations (what are referred to as “proper tail calls”) merged were: 41 | 42 | - Portability issues; LLVM at the time didn’t support proper tail calls when targeting certain architectures, notably MIPS and WebAssembly. 43 | - The fact that proper tail calls in LLVM were actually likely to cause a performance penalty due to how they were implemented at the time. 44 | - TCO makes debugging more difficult since it overwrites stack values. 45 | 46 | Indeed, the author of the RFC admits that Rust has gotten on perfectly fine thus far without TCO, and that it will certainly continue on just fine without it. 47 | 48 | Thus far, explicit user-controlled TCO hasn’t made it into rustc. 49 | 50 | ## Enabling TCO via a Library 51 | 52 | However, many of the issues that bog down TCO RFCs and proposals can be sidestepped to an extent. Several homebrew solutions for adding explicit TCO to Rust exist. 53 | 54 | The general idea with these is to implement what is called a “trampoline”. This refers to the abstraction that actually takes a tail-recursive function and transforms it to use an iterative loop instead. 55 | 56 | > How about we first implement this with a trampoline as a slow cross-platform fallback 57 | implementation, and then successively implement faster methods for each architecture/platform? 58 | > This way the feature can be ready quite quickly, so people can use it for elegant programming. In a future version of rustc such code will magically become fast. 59 | - [@ConnyOnny][connyonny], [4](https://github.com/rust-lang/rfcs/issues/271#issuecomment-269255176) 60 | 61 | Bruno Corrêa Zimmermann’s [tramp.rs][tramp_rs] library is probably the most high-profile of these library solutions. Let’s take a peek under the hood and see how it works. 62 | 63 | ## Diving Into `tramp.rs` 64 | 65 | The tramp.rs library exports two macros, `rec_call!` and `rec_ret!`, that facilitate the same behavior as what the proposed `become` keyword would do: it allows the programmer to prompt the Rust runtime to execute the specified tail-recursive function via an iterative loop, thereby decreasing the memory cost of the function to a constant. 66 | 67 | The `rec_call!` macro is what kicks this process off, and is most analogous to what the `become` keyword would do if it were introduced into rustc: 68 | 69 | ```rust 70 | macro_rules! rec_call { 71 | ($call:expr) => { 72 | return BorrowRec::Call(Thunk::new(move || $call)); 73 | }; 74 | } 75 | ``` 76 | 77 | `rec_call!` makes use of two additional important constructs, `BorrowRec` and `Thunk`. 78 | 79 | ```rust 80 | enum BorrowRec<'a, T> { 81 | Ret(T), 82 | Call(Thunk<'a, BorrowRec<'a, T>>), 83 | } 84 | ``` 85 | 86 | The `BorrowRec` enum represents two possible states a tail-recursive function call can be in at any one time: either it hasn’t reached its base case yet, in which case we’re still in the `BorrowRec::Call` state, or it has reached a base case and has produced its final value(s), in which case we’ve arrived at the `BorrowRec::Ret` state. 87 | 88 | > Note: Modeling the state of recursive functions in this way, with a `Call` state representing the fact that the function is still recursing and a `Ret` state indicating that the function has arrived at a base case, reminds me a lot of how async Promises are modeled. 89 | > Promises can be in one of three states, "pending", meaning it's still waiting on an asynchronous operation, "resolved", meaning the asynchronous operation occurred successfully, and "rejected", meaning the asynchronous operation did not occur successfully. 90 | 91 | The `Call` variant of the `BorrowRec` enum contains the following definition for a `Thunk`: 92 | 93 | ```rust 94 | struct Thunk<'a, T> { 95 | fun: Box + 'a>, 96 | } 97 | ``` 98 | 99 | The `Thunk` struct holds on to a reference to the tail-recursive function, which is represented by the `FnThunk` trait. 100 | 101 | Lastly, this is all tied together with the `tramp` function: 102 | 103 | ```rust 104 | fn tramp<'a, T>(mut res: BorrowRec<'a, T>) -> T { 105 | loop { 106 | match res { 107 | BorrowRec::Ret(x) => break x, 108 | BorrowRec::Call(thunk) => res = thunk.compute(), 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | This receives as input a tail-recursive function contained in a `BorrowRec` instance, and continually calls the function so long as the `BorrowRec` remains in the `Call` state. Otherwise, when the recursive function arrives at the `Ret` state with its final computed value, that final value is returned via the `rec_ret!` macro. 115 | 116 | ## Are We TCO Yet? 117 | 118 | So that’s it right? `tramp.rs` is the hero we all needed to enable on-demand TCO in our Rust programs, right? 119 | 120 | I’m afraid not. 121 | 122 | While I really like how the idea of trampolining as a way to incrementally introduce TCO is presented in this implementation, [benchmarks][benchmarks] that [@timthelion][timthelion] has graciously already run indicate that using tramp.rs leads to a slight regression in performance compared to manually converting the tail-recursive function to an iterative loop. 123 | 124 | ![timthelion's benchmarks](assets/timthelion-comment.png) 125 | 126 | Part of what contributes to the slowdown of tramp.rs’s performance is likely, as [@jonhoo][jonhoo] points out, the fact that each `rec_call!` call allocates memory on the heap due to it calling `Thunk::new`: 127 | 128 | ![Jon Gjenset weighing in](assets/jonhoo-comment.png) 129 | 130 | So it turns that tramp.rs’s trampolining implementation doesn’t even actually achieve the constant memory usage that TCO promises! 131 | 132 | Ah well. Perhaps on-demand TCO will be added to rustc in the future. Or maybe not; it’s gotten by just fine without it thus far. 133 | 134 | ## Links and Citations 135 | 136 | 1: [https://stackoverflow.com/questions/42788139/es6-tail-recursion-optimisation-stack-overflow](https://stackoverflow.com/questions/42788139/es6-tail-recursion-optimisation-stack-overflow) 137 | 138 | 2: [http://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html](http://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html) 139 | 140 | 3: [https://github.com/rust-lang/rfcs/issues/271#issuecomment-271161622](https://github.com/rust-lang/rfcs/issues/271#issuecomment-271161622) 141 | 142 | 4: [https://github.com/rust-lang/rfcs/issues/271#issuecomment-269255176](https://github.com/rust-lang/rfcs/issues/271#issuecomment-269255176) 143 | 144 | [computerphile_video]: https://youtu.be/_JtPhF8MshA 145 | [stack_overflow_tc]: https://stackoverflow.com/questions/310974/what-is-tail-call-optimization 146 | [pr_81]: https://github.com/rust-lang/rfcs/pull/81 147 | [mailing_list]: https://mail.mozilla.org/pipermail/rust-dev/2013-April/003557.html 148 | [gh_issue]: https://github.com/rust-lang/rust/issues/217 149 | [rfc]: https://github.com/DemiMarie/rfcs/blob/become/0000-proper-tail-calls.md 150 | [tramp_rs]: https://crates.io/crates/tramp 151 | [benchmarks]: https://gitlab.com/timthelion/trampoline-rs/commit/84f6c843658c6c3a5893effa031ce734b910171c 152 | [jonhoo]: https://github.com/jonhoo 153 | [connyonny]: https://github.com/connyonny 154 | [timthelion]: https://github.com/timthelion 155 | -------------------------------------------------------------------------------- /posts/rust-representer/index.md: -------------------------------------------------------------------------------- 1 | # Some Learnings from Implementing a Normalizing Rust Representer 2 | _July 13th, 2020 | #rust | #exercism | #open_source_ 3 | 4 | I’ve been helping out and contributing to [exercism.io](https://exercism.io) for the past few months. As an open source platform for learning programming languages that supports Rust, Exercism aligns very well with all the things I’m currently passionate about: open source, teaching, and Rust. 5 | 6 | One of the most challenging hurdles the Exercism platform faces is the fact that students who opt in to receive mentor feedback on their work have to wait for a live person to get around to reviewing their submission. Decreasing wait times for students is thus an important metric to optimize for in order to improve the overall student experience on the platform. 7 | 8 | In this post I’ll be talking about a project I’ve been working on that aims to address this problem. I’ll also be discussing the learnings I’ve been taking away from working on this project, as it’s my first non-toy Rust project that I’ve undertaken. 9 | 10 | ## How Do We Decrease Wait Times for Students? 11 | 12 | There are a couple of factors that contribute to longer waiting times for students on the Exercism platform: the availability of mentors, the number of pending submissions in the queue, and how much time it takes for a mentor to compose their feedback response to any one exercise. 13 | 14 | Perhaps in the future, there will be a purely-autonomous system that is able to provide high quality tailored feedback in response to a student’s code submission. However, at this point in time, when it comes to giving human-centric feedback, I don’t believe computers are up to snuff yet. 15 | 16 | So the question becomes: how do we streamline the process through which Exercism mentors provide feedback to students? 17 | 18 | One thing that would help would be to reduce the set of possible configurations of student submissions, at least from the perspective of mentors. Submissions can be normalized by stripping away trivial aspects, such as formatting, comments, and variable names, that don’t contribute to making a submission unique as far as its logical approach. 19 | 20 | For example, given the following student implementation of an Exercism exercise called `two-fer` 21 | 22 | ```rust 23 | fn twofer(name: &str) -> String { 24 | match name { 25 | "" => "One for you, one for me.".to_string(), 26 | // use the `format!` macro to return a formatted String 27 | _ => format!("One for {}, one for me.", name), 28 | } 29 | } 30 | ``` 31 | 32 | would be transformed into the following: 33 | 34 | ```rust 35 | fn PLACEHOLDER_1(PLACEHOLDER_2: &str) -> String { 36 | match PLACEHOLDER_2 { 37 | "" => "One for you, one for me.".to_string(), 38 | _ => format!("One for {}, one for me.", PLACEHOLDER_2), 39 | } 40 | } 41 | ``` 42 | 43 | The idea is that the normalized representation would make it easier for mentors to focus on the overall structure and shape of the solution, so that they'd be able to give higher-level feedback. 44 | 45 | For example, with the following normalized representation of an alternative solution to this exercise, a mentor would be able to focus more quickly on the fact that the student used an if-else with explicit `return`s, neither of which are idiomatic in Rust, and would be able to provide feedback to that effect. 46 | 47 | ```rust 48 | fn PLACEHOLDER_1(PLACEHOLDER_2: &str) -> String { 49 | if PLACEHOLDER_2 == "" { 50 | return "One for you, one for me.".to_string(); 51 | } else { 52 | return format!("One for {}, one for me.", PLACEHOLDER_2); 53 | } 54 | } 55 | ``` 56 | 57 | The program that performs this normalization is called a representer. In Exercism, each language track will have its own representer to handle submissions for that particular track. When I noticed that the Rust track didn’t yet have a representer that was being worked on, I jumped at the chance to work on it! 58 | 59 | ## Implementing a Rust Representer 60 | 61 | At a high level, the steps a normalizing representer needs to take are the following: 62 | 63 | 1. Parse the source code into an abstract syntax tree. 64 | 2. Apply transformations to the appropriate tree nodes. 65 | 3. Turn the tree back into a string representing the transformed source code. 66 | 67 | The `syn` [crate](https://docs.rs/syn/1.0.33/syn/index.html) is perhaps one of the most robust libraries available in the Rust ecosystem for parsing and transforming Rust code. It’s primary use-case is as a utility for implementing procedural macros in Rust, but judging from its thorough documentation, it seemed full-featured enough to handle the use-case I had in mind. 68 | 69 | The first step I took was to use the functionality provided by `syn` to parse some source code into an AST, print the AST, and then turn that AST back into a string: 70 | 71 | ```rust 72 | use std::env; 73 | use std::fs::File; 74 | use std::io::prelude::*; 75 | use std::io::Read; 76 | use std::process; 77 | 78 | fn main() -> Result<(), Box> { 79 | let output_path = “./output.rs”; 80 | let mut args = end::args(); 81 | let _ = args.next(); 82 | 83 | let filename = match (args.next(), args.next()) { 84 | (Some(filename), None) => filename, 85 | _ => { 86 | eprintln!(“Usage: representer path/to/filename.rs”); 87 | process::exit(1); 88 | } 89 | }; 90 | 91 | let mut input = File::open(&filename)?; 92 | let mut src = String::new(); 93 | input.read_to_string(&mut src)?; 94 | 95 | let syntax = syn::parse_file(&src)?; 96 | println!(“{:?}”, syntax); 97 | 98 | let mut output = File::create(output_path)?; 99 | output.write(syntax.to_string().as_bytes())?; 100 | 101 | Ok(()) 102 | } 103 | ``` 104 | 105 | Starting off with this sets up a nice boundary in which we can work within. Now we’ll need to work out how we’ll traverse the AST and transform the relevant tree nodes. 106 | 107 | ## `syn`’s `VisitMut` Trait 108 | 109 | The `syn` crate provides an extremely handy trait called `VisitMut`, which provides the user with a lot of flexibility when it comes to traversing and mutating AST nodes in-place. The `VisitMut` trait exposes a whole slew of methods for accessing every type of AST node that `syn` differentiates. 110 | 111 | ![Just some of the methods on the `visit_mut` trait](assets/visit_mut_methods.png) 112 | 113 | A logical first place to start would be replacing the identifier name of a `let` binding, taking something as simple as 114 | 115 | ```rust 116 | let x = 5; 117 | ``` 118 | 119 | and transforming it into 120 | 121 | ```rust 122 | let PLACEHOLDER = 5; 123 | ``` 124 | 125 | The `visit_pat_ident_mut` method is what we’re looking for. It gives us access to AST nodes that represent variable binding patterns, of which `let` bindings are one of. 126 | 127 | Since `visit_pat_ident_mut` is a trait method, we need to implement the `VisitMut` trait on a type so that the method’s behavior can be overwritten. To start off with, I defined an empty struct for this purpose: 128 | 129 | ```rust 130 | use proc_macro2::{Ident, Span}; 131 | use syn::visit_mut::VisitMut; 132 | 133 | struct IdentVisitor; 134 | 135 | impl VisitMut for IdentVisitor { 136 | fn visit_pat_ident_mut(&mut self, node: &mut PatIdent) { 137 | // replace the node’s ident field with “PLACEHOLDER” 138 | self.ident = Ident::new(“PLACEHOLDER”, Span::call_site()); 139 | } 140 | } 141 | ``` 142 | 143 | Lo and behold, this simple logic worked! Top-level let bindings were traversed, with their variable names replaced with `"PLACEHOLDER"`. 144 | 145 | ## Generating Placeholder IDs 146 | 147 | Turning our attention to replacing multiple variable names, we’ll need to generate IDs for each placeholder, so that mentors are able to differentiate between bindings with different patterns in the original submission. 148 | 149 | One way to accommodate this is to store a global `HashMap` of all the generated mappings. This way, we can tell if an identifier has been encountered before, in which we’ll re-use the same generated placeholder; otherwise a new placeholder will be generated. 150 | 151 | This is where our thus-far empty `IdentVisitor` struct comes in handy: we’ll define the hash map as a field on it. 152 | 153 | ```rust 154 | ... 155 | use std::collections::HashMap; 156 | use std::collections::hash_map::Entry; 157 | 158 | const PLACEHOLDER: &str = “PLACEHOLDER_”; 159 | 160 | struct IdentVisitor { 161 | mappings: HashMap, 162 | // a monotonically-increasing counter for new placeholders 163 | uid: u32, 164 | } 165 | 166 | impl IdentVisitor { 167 | fn new() -> Self { 168 | IdentVisitor { 169 | mappings: HashMap::new(), 170 | uid: 0, 171 | } 172 | } 173 | ``` 174 | 175 | We’ll then also add a method to either fetch a pre-existing placeholder, or generate a new one if the identifier doesn’t yet exist as a mapping: 176 | 177 | ```rust 178 | impl IdentVisitor { 179 | ... 180 | 181 | fn get_or_insert_mapping(&mut self, ident: String) -> String { 182 | let uid = match self.mappings.entry(ident) { 183 | Entry::Occupied(o) => o.into_mut(), 184 | Entry::Vacant(v) => { 185 | self.uid += 1; 186 | v.insert(self.uid); 187 | } 188 | }; 189 | 190 | format!(“{}{}”, PLACEHOLDER, uid) 191 | } 192 | } 193 | ``` 194 | 195 | We can now refactor our `visit_pat_ident_mut` method to use `get_or_insert_mapping`: 196 | 197 | ```rust 198 | impl VisitMut for IdentVisitor { 199 | fn visit_pat_ident_mut(&mut self, node: &mut PatIdent) { 200 | let placeholder = self.get_or_insert_mapping(node.ident.to_string()); 201 | self.ident = Ident::new(placeholder, Span::call_site()); 202 | } 203 | } 204 | ``` 205 | 206 | Huzzah! Multiple let bindings are now replaced with placeholders that each have a different ID: 207 | 208 | ```rust 209 | let PLACEHOLDER_1 = 5; 210 | let mut PLACEHOLDER_2 = 15; 211 | ``` 212 | 213 | ## Eliminating Duplicated Code with Traits 214 | 215 | With variable bindings out of the way, much of the way forward involves accessing every other type of AST node that contains identifiers that we’d like to replace: struct names, enum names, user-defined types, function definitions, etc. 216 | 217 | Struct names can be accessed via the `visit_item_struct_mut` method. Recycling the same logic we used for transforming let binding identifiers, we can write: 218 | 219 | ```rust 220 | impl VisitMut for IdentVisitor { 221 | ... 222 | 223 | fn visit_item_struct_mut(&mut self, node: &mut ItemStruct) { 224 | let placeholder = self.get_or_insert_mapping(node.ident.to_strong()); 225 | self.ident = Ident::new(placeholder, Span::call_site()); 226 | } 227 | } 228 | ``` 229 | 230 | For certain types of AST nodes (such as the `Expr` type that encapsulates Rust expressions), the logic can certainly get more complicated, but this is the general gist of the overall pattern. 231 | 232 | One piece of advice I was given by a much more experienced Rust developer was to implement a new trait to represent the ability of certain nodes to have their identifier replaced. While doing such a thing won’t actually allow us to significantly reduce the total SLOCs of the implementation as it stands now, it will improve future maintainability of the codebase. Plus, I’d never really been presented with a relevant opportunity to implement a Rust trait, so I jumped at the chance to do it in a “real” Rust project. 233 | 234 | The trait I ended up implementing exposes two methods: `ident_string`, which ensures that the AST node has an identifier that can be replaced, and `set_ident`, which replaces the node’s identifier with a specified string: 235 | 236 | ```rust 237 | trait ReplaceIdentifier { 238 | fn ident_string(&self) -> String; 239 | fn set_ident(&mut self, ident: String); 240 | } 241 | ``` 242 | 243 | Now, we’ll implement this trait on nodes that have an identifier, which include the aforementioned `PatIdent` and `ItemStruct`, as well as many others: 244 | 245 | ```rust 246 | impl ReplaceIdentifier for PatIdent { 247 | fn ident_string(&self) -> String { 248 | self.ident.to_string() 249 | } 250 | 251 | fn set_ident(&mut self, ident: String) { 252 | self.ident = Ident::new(ident, Span::call_site()); 253 | } 254 | } 255 | 256 | impl ReplaceIdentifier for ItemStruct { 257 | fn ident_string(&self) -> String { 258 | self.ident.to_string() 259 | } 260 | 261 | fn set_ident(&mut self, ident: String) { 262 | self.ident = Ident::new(ident, Span::call_site()); 263 | } 264 | } 265 | ``` 266 | 267 | I then opted to implement a `replace_identifier` method for any type that implements the `ReplaceIdentifier` trait: 268 | 269 | ```rust 270 | impl IdentVisitor { 271 | ... 272 | 273 | fn replace_identifier(&mut self, node: Node) { 274 | let ident_string = node.ident_string(); 275 | let identifier = self.get_or_insert_mapping(ident_string); 276 | node.set_ident(identifier); 277 | } 278 | } 279 | ``` 280 | 281 | Following this, we’ll change our various `visit_mut` methods to utilize `replace_identifier`: 282 | 283 | ```rust 284 | impl IdentVisitor { 285 | ... 286 | 287 | fn visit_pat_ident_mut(&mut self, node: &mut PatIdent) { 288 | self.replace_identifier(node); 289 | } 290 | 291 | fn visit_item_struct_mut(&mut self, node: &mut ItemStruct) { 292 | self.replace_identifier(node); 293 | } 294 | } 295 | ``` 296 | 297 | Refactoring the code in this way clearly separates concerns: the `visit_mut` methods access the AST nodes we care about, the `ReplaceIdentifier` trait lays out the contract that nodes need to adhere to, which the `replace_identifier` method can then act upon. 298 | 299 | It was so cool to me to have gotten an opportunity to utilize traits in such a way when implementing this project. Before this, I’d only ever had a vague understanding of traits. Using them in this way in this project really served to hammer home the point of why traits are useful and when are appropriate times to use them. 300 | 301 | While the portrait of the representer that I’ve painted in this post is quite simplified (some types of Rust syntax, such as expressions, are a bit more complex to handle), it preserves the kernel of how the representer functions. 302 | 303 | ## Generaing Boilerplate with a Procedural Macro 304 | 305 | After hitting the initial MVP, I've since stepped away working on the Rust representer and handed off maintenance to the other Exercism Rust maintainers. One feature extension I've thought of has to do with all of the redundant boilerplate that exists to implement the ReplaceIdentifier trait for different AST nodes; if you take a look at it [here](https://github.com/exercism/rust-representer/blob/main/src/replace_identifier.rs), you'll notice almost all of the trait implementations are identical. A procedural macro could be defined to generate this boilerplate, such that we'd simply be able to annotate the different AST node types, which would be pretty slick. 306 | 307 | The way this would probably work is we'd define an enum whose variants are all of the `syn` identifier types that we want to implement the `ReplaceIdentifier` trait for, then simply annotate our enum with a `#[derive(ReplaceIdentifier)]` statement: 308 | 309 | ```rust 310 | use syn::{ 311 | ItemEnum, ItemStruct, ItemTrait, PatIdent, ... 312 | }; 313 | 314 | #[derive(ReplaceIdentifier)] 315 | enum ToReplace { 316 | ItemEnum, 317 | ItemStruct, 318 | ItemTrait, 319 | PatIdent, 320 | ... 321 | } 322 | ``` 323 | 324 | Here's what an implementation of this procedural macro could look like: 325 | ```rust 326 | #[proc_macro_derive(ReplaceIdentifier)] 327 | pub fn derive(input: TokenStream) -> TokenStream { 328 | let ast = parse_macro_input!(input as DeriveInput); 329 | 330 | let mut impls = vec![]; 331 | 332 | match ast.data { 333 | Data::Enum(DataEnum { variants, .. }) => { 334 | for variant in variants { 335 | match variant.fields { 336 | Fields::Unit => { 337 | let vident = variant.ident; 338 | impls.push(quote! { 339 | impl ReplaceIdentifier for #vident { 340 | fn ident_string(&self) -> String { 341 | self.ident.to_string() 342 | } 343 | 344 | fn set_ident(&mut self, ident: String) { 345 | self.ident = syn::Ident::new(&ident, syn::Span::call_site()); 346 | } 347 | } 348 | }); 349 | } 350 | _ => {} 351 | } 352 | } 353 | } 354 | _ => {} 355 | } 356 | 357 | let expanded = quote! { #(#impls)* }; 358 | 359 | expanded.into() 360 | } 361 | ``` 362 | 363 | And lo and behold, it would generate an `impl ReplaceIdentifier` block for each of the variants in the enum, complete with all of the necessary trait methods! 364 | 365 | ## In Closing 366 | 367 | If you’re interested in contributing to this sort of project, or helping out as a mentor for those looking to sharpen their programming skills, [Exercism](https://exercism.io) is always looking for more open source contributors, maintainers, and mentors, especially with our in-progress [V3](https://github.com/exercism/v3) push! 368 | -------------------------------------------------------------------------------- /posts/extending-minigrep/index.md: -------------------------------------------------------------------------------- 1 | # A Beginner's Guide to Handling Errors in Rust I: Minigrep 2 | 3 | _April 5th, 2021 · #rust · #error_handling · #beginner_ 4 | 5 | The example projects in _The Rust Programming Language_ are great for introducing new would-be Rustaceans to different aspects and features of Rust. In this post, we'll be looking at some different ways of implementing a more robust error handling infrastructure by extending the `minigrep` project from _The Rust Programming Language_. 6 | 7 | The `minigrep` project is introduced in [chapter 12][ch12] and walks the reader through building a simple version of the `grep` command line tool, which is a utility for searching through text. For example, you'd pass in a query, the text you're searching for, along with the file name where the text lives, and get back all of the lines that contain the query text. 8 | 9 | The goal of this post is to extend the book's `minigrep` implementation with more robust error handling patterns so that you'll have a better idea of different ways to handle errors in a Rust project. 10 | 11 | For reference, you can find the final code for the book's version of `minigrep` [here][minigrep-orig]. 12 | 13 | ## Error handling use cases 14 | 15 | A common pattern when it comes to structuring Rust projects is to have a "library" portion where the primary data structures, functions, and logic live and an "application" portion that ties the library functions together. 16 | 17 | You can see this in the file structure of the original `minigrep` code: the application logic lives inside of the `src/bin/main.rs` file, and it's merely a thin wrapper around data structures and functions that are defined in the `src/lib.rs` file; all the `main` function does is call `minigrep::run`. 18 | 19 | This is important to point out because depending on whether we're building an application or a library changes how we approach error handling. 20 | 21 | When it comes to an application, the end user most likely doesn't want to know about the nitty gritty details of what caused an error. Indeed, the end user of an application probably only ought to be notified of an error in the event that the error is _unrecoverable_. In this case, it's also useful to provide details on why the unrecoverable error occurred, especially if it has to do with user input. If some sort of recoverable error happened in the background, the consumer of an application probably doesn't need to know about it. 22 | 23 | Conversely, when it comes to a library, the end users are other developers who are using the library and building something on top of it. In this case, we'd like to give as many relevant details about any errors that occurred in our library as possible. The consumer of the library will then decide how they want to handle those errors. 24 | 25 | So how do these two approaches play together when we have both a library portion and an application portion in our project? The `main` function executes the `minigrep::run` function and outputs any errors that crop up as a result. So most of our error handling efforts will be focused on the library portion. 26 | 27 | ## Surfacing library errors 28 | 29 | In `src/lib.rs`, we have two functions, `Config::new` and `run`, which might return errors: 30 | ```rust 31 | impl Config { 32 | pub fn new(mut args: env::Args) -> Result { 33 | args.next(); 34 | 35 | let query = match args.next() { 36 | Some(arg) => arg, 37 | None => return Err("Didn't get a query string"), 38 | }; 39 | 40 | let filename = match args.next() { 41 | Some(arg) => arg, 42 | None => return Err("Didn't get a file name"), 43 | }; 44 | 45 | let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); 46 | 47 | Ok(Config { 48 | query, 49 | filename, 50 | case_sensitive, 51 | }) 52 | } 53 | } 54 | 55 | pub fn run(config: Config) -> Result<(), Box> { 56 | let contents = fs::read_to_string(config.filename)?; 57 | 58 | let results = if config.case_sensitive { 59 | search(&config.query, &contents) 60 | } else { 61 | search_case_insensitive(&config.query, &contents) 62 | }; 63 | 64 | for line in results { 65 | println!("{}", line); 66 | } 67 | 68 | Ok(()) 69 | } 70 | ``` 71 | 72 | There are exactly three spots where errors are being returned: two errors occur in the `Config::new` function, which returns a `Result`. In this case, the error variant of the `Result` is a static string slice. 73 | 74 | Here we return an error when a query is not provided by the user. 75 | ```rust 76 | let query = match args.next() { 77 | Some(arg) => arg, 78 | None => return Err("Didn't get a query string"), 79 | }; 80 | ``` 81 | 82 | Here we return an error when a filename is not provided by the user. 83 | ```rust 84 | let filename = match args.next() { 85 | Some(arg) => arg, 86 | None => return Err("Didn't get a file name"), 87 | }; 88 | ``` 89 | 90 | The main problem with structuring our errors in this way as static strings is that the error messages are not located in a central spot where we can easily refactor them should we need to. It also makes it more difficult to keep our error messages consistent between the same types of errors. 91 | 92 | The third error occurs at the top of `run` function, which returns a `Result<(), Box>`. The error variant in this case is a [trait object][trait-object] that implements the `Error` [trait][error-trait]. In other words, the error variant for this function is any instance of a type that implements the `Error` trait. 93 | 94 | Here we bubble up any errors that might have occurred as a result of calling `fs::read_to_string`. 95 | ```rust 96 | let contents = fs::read_to_string(config.filename)?; 97 | ``` 98 | 99 | This works for the errors that might crop up as a result of calling `fs::read_to_string` since this function is capable of returning multiple types of errors. Therefore, we need a way to generically represent those different possible error types; the commonality between them all is the fact that they all implement the `Error` trait! 100 | 101 | Ultimately, what we want to do is define all of these different types of errors in a central location and have them all be variants of a single type. 102 | 103 | ### Defining error variants in a central type 104 | 105 | We'll create a new `src/error.rs` file and define an enum `AppError`, deriving the `Debug` trait in the process so that we can get a debug representation should we need it. We'll name each of the variants of this enum in such a way that they appropriately represent each of the three types of errors: 106 | 107 | ```rust 108 | #[derive(Debug)] 109 | pub enum AppError { 110 | MissingQuery, 111 | MissingFilename, 112 | ConfigLoad, 113 | } 114 | ``` 115 | 116 | The third variant, `ConfigLoad`, maps to the error that might crop up when calling `fs::read_to_string` in the `Config::run` function. This might seem a bit out of place at first, since if an error occurs with that function, it would be some sort of I/O problem reading the provided config file. So why didn't we name it `IOError` or something like that? 117 | 118 | In this case, since we're surfacing an error from a standard library function, it's more relevant to our application to describe how the surfaced error affects it, instead of simply reiterating it. When an error occurs with `fs::read_to_string`, that prevents our `Config` from loading, so that's why we named it `ConfigLoad`. 119 | 120 | Now that we have this type, we need to update all of the spots in our code where we return errors to utilize this `AppError` enum. 121 | 122 | ### Returning variants of our `AppError` 123 | 124 | At the top of our `src/lib.rs` file, we need to declare our `error` module and bring `error::AppError` into scope: 125 | ```rust 126 | mod error; 127 | 128 | use error::AppError; 129 | ``` 130 | 131 | In our `Config::new` function, we need to update the spots where we were returning static string slices as errors, as well as the return type of the function itself: 132 | 133 | ```diff 134 | - pub fn new(mut args: env::Args) -> Result 135 | + pub fn new(mut args: env::Args) -> Result 136 | // --snip-- 137 | 138 | let query = match args.next() { 139 | Some(arg) => arg, 140 | - None => return Err("Didn't get a query string"), 141 | + None => return Err(AppError::MissingQuery), 142 | }; 143 | 144 | let filename = match args.next() { 145 | Some(arg) => arg, 146 | - None => return Err("Didn't get a file name"), 147 | + None => return Err(AppError::MissingFilename), 148 | }; 149 | 150 | // --snip-- 151 | ``` 152 | 153 | The third error in the `run` function only requires us to update its return type, since the `?` operator is already taking care of bubbling the error up and returning it should it occur. 154 | 155 | ```diff 156 | - pub fn run(config: Config) -> Result<(), Box> 157 | + pub fn run(config: Config) -> Result<(), AppError> 158 | ``` 159 | 160 | Ok, so we're now making use of our error variants, which, should they occur, are being surfaced to our `main` function and printed out. But we no longer have the actual error messages that we had before defined anywhere! 161 | 162 | ### Annotating error variants with `thiserror` 163 | 164 | The `thiserror` [crate][thiserror] is one that is commonly used to provide an ergonomic way to format error messages in a Rust library. 165 | 166 | It allows us to annotate each of the variants in our `AppError` enum with the actual error message that we want displayed to the end user. 167 | 168 | Let's add it as a dependency in our Cargo.toml: 169 | 170 | ``` 171 | [dependencies] 172 | thiserror = "1" 173 | ``` 174 | 175 | In `src/error.rs` we'll bring the `thiserror::Error` trait into scope and have our `AppError` type derive it. We need this trait derived in order to annotate each enum variant with an `#[error]` block. Now we specify the error message that we want displayed for each particular variant: 176 | 177 | ```diff 178 | + use std::io; 179 | 180 | + use thiserror::Error; 181 | 182 | - #[derive(Debug)] 183 | + #[derive(Debug, Error)] 184 | pub enum AppError { 185 | + #[error("Didn't get a query string")] 186 | MissingQuery, 187 | + #[error("Didn't get a file name")] 188 | MissingFilename, 189 | + #[error("Could not load config")] 190 | ConfigLoad { 191 | + #[from] 192 | + source: io::Error, 193 | + } 194 | } 195 | ``` 196 | 197 | What's all the extra stuff was added to the `ConfigLoad` variant? Since a `ConfigLoad` error only occurs when there's an underlying error with the call to `fs::read_to_string`, what the `ConfigLoad` variant is actually doing is providing extra context around the underlying I/O error. 198 | 199 | `thiserror` allows us to wrap a lower-level error in additional context by annotating it with a `#[from]` in order to convert the `source` into our homebrew error type. In this way, when an I/O error occurs (like when we specify a file to search through that doesn't actually exist), we get an error like this: 200 | 201 | ``` 202 | Could not load config: Os { code: 2, kind: NotFound, message: "No such file or directory" } 203 | ``` 204 | 205 | Without it, the resulting error message looks like this: 206 | 207 | ``` 208 | Os { code: 2, kind: NotFound, message: "No such file or directory" } 209 | ``` 210 | 211 | To a consumer of our library, it's harder to figure out the source of this error; the additional context helps a lot. 212 | 213 | You can find the version of `minigrep` that uses `thiserror` [here][minigrep-thiserror]. 214 | 215 | ## A more manual approach 216 | 217 | Now we'll switch gears and look out how we might achieve the same results that `thiserror` provides us, but without bringing it in as a dependency. 218 | 219 | Under the hood, `thiserror` performs some magic with procedural macros, which can have a noticeable effect on compilation speeds. In the case of `minigrep`, we have very few error variants and the project is so small that a dependency on `thiserror` really won't introduce much of an increase in compilation time, but it could be a consideration in a much larger and more complex project. 220 | 221 | So on that note, we'll wrap up this post by ripping it out and replacing it with our own hand-rolled implementation. The nice thing about going down this route is that we'll only need to make changes to the `src/error.rs` file to implement all of the necessary changes (besides, of course, removing `thiserror` from our Cargo.toml). 222 | 223 | ```diff 224 | [dependencies] 225 | - thiserror = "1" 226 | ``` 227 | 228 | Let's remove all of the annotations that `thiserror` was providing us. We'll also replace the `thiserror::Error` trait with the `std::error::Error` trait: 229 | 230 | ```diff 231 | - use thiserror::Error; 232 | + use std::error::Error; 233 | 234 | - #[derive(Debug, Error)] 235 | + #[derive(Error)] 236 | pub enum AppError { 237 | - #[error("Didn't get a query string")] 238 | MissingQuery, 239 | - #[error("Didn't get a file name")] 240 | MissingFilename, 241 | - #[error("Could not load config")] 242 | ConfigLoad { 243 | - #[from] 244 | source: io::Error, 245 | } 246 | } 247 | ``` 248 | 249 | In order to get back all of the functionality we just wiped, we'll need to do three things: 250 | 251 | 1. Implement the `Display` [trait][display-docs] for `AppError` so that our error variants can be displayed to the user. 252 | 2. Implement the `Error` [trait][error-docs] for `AppError`. This trait represents the basic expectations of an error type, namely that they implement `Display` and `Debug`, plus the capability to fetch the underlying source or cause of the error. 253 | 3. Implement `From` for `AppError`. This is required so that we can convert an I/O error returned from `fs::read_to_string` into an instance of `AppError`. 254 | 255 | Here's our implementation of the `Display` trait for our `AppError`. It maps each error variant to an string and writes it to the `Display` formatter. 256 | 257 | ```rust 258 | use std::fmt; 259 | 260 | impl fmt::Display for AppError { 261 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 262 | match self { 263 | Self::MissingQuery => f.write_str("Didn't get a query string"), 264 | Self::MissingFilename => f.write_str("Didn't get a file name"), 265 | Self::ConfigLoad { source } => write!(f, "Could not load config: {}", source), 266 | } 267 | } 268 | } 269 | ``` 270 | 271 | And here's our implementation of the `Error` trait. The main method to be implemented is the `Error::source` method, which is meant to provide information about the source of an error. For our `AppError` type, only `ConfigLoad` exposes any underlying source information, namely the I/O error that might happen as a result of calling `fs::read_to_string`. There's no underlying source information to expose in the case of the other error variants. 272 | 273 | ```rust 274 | use std::error; 275 | 276 | impl error::Error for AppError { 277 | fn source(&self) -> Option<&(dyn Error + 'static)> { 278 | match self { 279 | Self::ConfigLoad { source } => Some(source), 280 | _ => None, 281 | } 282 | } 283 | } 284 | ``` 285 | 286 | The `&(dyn Error + 'static)` part of the return type is similar to the `Box` trait object that we saw earlier. The main difference here is that the trait object is behind an immutable reference instead of a `Box` pointer. The `'static` lifetime here means the trait object itself only contains owned values, i.e., it doesn't store any references internally. This is necessary in order to assuage the compiler that there's no chance of a dangling pointer here. 287 | 288 | Lastly, we need a way to convert an `io::Error` into an `AppError`. We'll do this by impling `From for AppError`. 289 | 290 | ```rust 291 | impl From for AppError { 292 | fn from(source: io::Error) -> Self { 293 | Self::ConfigLoad { source } 294 | } 295 | } 296 | ``` 297 | 298 | There's not much to this one. If we get an `io::Error`, all we do to convert it to an `AppError` is wrap it in a `ConfigLoad` variant. 299 | 300 | And that's all folks! You can find this version of our minigrep implementation [here][minigrep-manual]. 301 | 302 | ## Summary 303 | 304 | In closing, we discussed how the original `minigrep` implementation presented in *The Rust Programming Language* book is a bit lacking in the error handling department, as well as how to think about different error handling use cases. 305 | 306 | From there, we showcased how to use the `thiserror` crate to centralize all of the possible error variants into a single type. 307 | 308 | Finally, we peeled back the veneer that `thiserror` provides and showed how to replicate the same functionality manually. 309 | 310 | I hope you all learned something from this post! 🙂 311 | 312 | 313 | [ch12]: https://doc.rust-lang.org/book/ch12-00-an-io-project.html 314 | [minigrep-orig]: https://github.com/seanchen1991/error-handling-examples/tree/minigrep-control/examples/minigrep 315 | [minigrep-thiserror]: https://github.com/seanchen1991/error-handling-examples/tree/minigrep-thiserror/examples/minigrep 316 | [minigrep-manual]: https://github.com/seanchen1991/error-handling-examples/tree/main/examples/minigrep 317 | [trait-object]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#defining-a-trait-for-common-behavior 318 | [error-trait]: https://doc.rust-lang.org/std/error/trait.Error.html 319 | [thiserror]: https://docs.rs/thiserror/1.0.24/thiserror/ 320 | [display-docs]: https://doc.rust-lang.org/std/fmt/trait.Display.html 321 | [error-docs]: https://doc.rust-lang.org/std/error/trait.Error.html 322 | -------------------------------------------------------------------------------- /posts/lru-cache/index.md: -------------------------------------------------------------------------------- 1 | # Implementing an LRU Cache in Rust 2 | _January 23rd, 2021 | #rust | #python | #data_structures | #intermediate_ 3 | 4 | > Note: This post assumes familiarity with traits in Rust. If you aren’t familiar with them, I’d recommend reading more about them. The [chapter](https://docs.rust-lang.org/book/ch10-02-traits.html) 5 | in the official Rust book is as good a place as any 🙂 6 | 7 | LRU caches are one of my favorite data structures to teach. One way of implementing an LRU cache (in Python, the language that I teach in) requires combining two separate data structures: a hash map in order to enable efficient access via hashing of keys, and a doubly-linked list in order to maintain the ordering of cache elements. 8 | 9 | However, structuring an LRU cache implementation in the same way in Rust would likely make it really clunky and difficult, since [implementing doubly linked lists in Rust is hard][too-many-lists]. We’ll need to come up with a different strategy when we get around to implementing it Rust. But first, let’s see the Python implementation. 10 | 11 | > Note: I’m aware that Python’s dictionary implementation orders key-value pairs by insertion order, so we could just take advantage of that feature in our LRU cache implementation as a way to keep track of the order of cache entries. However, this would obviate the learning exercise. We’ll be disregarding this feature for our Python implementation. 12 | 13 | # Coming up with Requirements for our Cache 14 | 15 | The job of a cache is to store data and/or objects such that subsequent accesses are performed more quickly than the initial access. By design, caches are meant to be kept small in order to speed up read operations. An important question then when designing a cache is “what gets kept and what gets evicted when the cache reaches capacity?”. 16 | 17 | An LRU (or Least Recently Used) cache employs a fairly straightforward strategy: the data that was accessed the longest time ago is what is evicted from the cache. In order to do this, our LRU cache needs to keep track of the order in which elements (which take the form of key-value pairs) it holds are inserted and fetched. 18 | 19 | Whenever an element is inserted into or fetched from the cache, that element becomes the newest element in the cache. Additionally, if the cache is already at max capacity when we insert a new element, then the eviction routine needs to be executed in order to remove the oldest cache element and make room for the new cache element. 20 | 21 | So our LRU cache will need an `insert` operation as well as a `fetch` operation. It will also need an `evict` operation, though we’ll opt to not expose this method as part of our cache’s public API. 22 | 23 | # Our Python Implementation 24 | 25 | With a loose specification of the requirements of our LRU cache, we can define the constructor as such: 26 | 27 | ```python 28 | from doubly_linked_list import DoublyLinkedList 29 | 30 | class LRUCache: 31 | def __init__(self, capacity=100): 32 | # the max number of entries the cache can hold 33 | self.capacity = capacity 34 | # the hash map for storing entries as key-value pairs 35 | # it’s what allows us to efficiently fetch entries 36 | self.storage = dict() 37 | # a doubly linked list for keeping track of the order 38 | # of elements in our cache 39 | self.order = DoublyLinkedList() 40 | ``` 41 | 42 | > Note: We won’t be going over the design and implementation of the `DoublyLinkedList` that we’re using here, though its code is included in the GitHub repo should you wish to take a look. 43 | 44 | Our cache’s constructor receives a single optional parameter specifying the maximum number of entries that the cache can hold, which is stored in the `self.capacity` variable. The `self.storage` dictionary associates keys with the contents of the cache that we actually care about. `self.order` stores a `DoublyLinkedList` whose sole job is to keep track of the order of entries in the cache: the newest entry in the list will always be at the head of the list, and the oldest entry will always be at the tail. 45 | 46 | Let’s first define our `insert` method. It takes a key-value pair, where the value is the actual cached content that we care about, and the key is some sort of key that allows us to access the cached content. If the key isn’t already contained in the cache, it needs to be added to the `self.storage` dictionary, and we’ll also add the key as a node at the head of the `self.order` linked list. However, if the key already exists in the cache, then we want to only overwrite its value instead of re-inserting the key into the cache with a new value. 47 | 48 | Additionally, if it turns out the cache is already at max capacity, we’ll need to evict the oldest entry in the cache: 49 | 50 | ```python 51 | def insert(self, key, value): 52 | # if the key is already in the cache, overwrite 53 | # its value 54 | if key in self.storage: 55 | entry = self.storage[key] 56 | entry.data = (key, value) 57 | # touch this entry to move it to the head of 58 | # the linked list 59 | self.touch(entry) 60 | return 61 | 62 | # check if our cache is at max capacity to see 63 | # if we need to evict the oldest entry 64 | if len(self.storage) == self.capacity: 65 | self.evict() 66 | 67 | # add the key and value as a node at the 68 | # head of our doubly linked list 69 | self.order.add_to_head((key, value)) 70 | # add the linked list node as the value of 71 | # the key in our storage dictionary 72 | self.storage[key] = self.order.head 73 | ``` 74 | 75 | Each linked list node is storing a tuple of the `key` and the `value`. Each key-value pair in `self.storage` consists of the `key` as its key and a linked list node as its value: 76 | 77 | ![Layout of the Python LRU implementation](assets/python-impl-layout.png) 78 | 79 | Let’s implement the `touch` method, which is responsible for moving an entry in our cache to the most-recently-added spot in our cache. Our doubly linked list implementation has a method `move_to_front` that takes a node and does the work of moving it from wherever it is in the list to the head; we’ll use it here in our `touch` implementation: 80 | 81 | ```python 82 | def touch(self, entry): 83 | self.order.move_to_front(entry) 84 | ``` 85 | 86 | The `evict` method is responsible for removing the oldest entry in the cache and is called only when we attempt to insert into the cache when it is already at max capacity. It needs to remove the node at the tail of the linked list, as well as make sure the key that referred to the oldest entry is also removed from `self.storage`: 87 | 88 | ```python 89 | def evict(self): 90 | # delete the key-value pair from the storage dict 91 | # we can get the oldest entry’s key by accessing 92 | # it from the tail of the linked list 93 | key_to_delete = self.order.tail.data[0] 94 | del self.storage[key_to_delete] 95 | 96 | # remove the tail entry from our linked list 97 | self.order.remove_from_tail() 98 | ``` 99 | 100 | Now we need to implement the `fetch` method which accepts a key to an entry in the cache, checks whether the key exists, then returns the value associated with the key. This method also moves the entry to the head of the linked list as this entry is now the most-recently-used entry in the cache. 101 | 102 | ```python 103 | def fetch(self, key): 104 | if key not in self.storage: 105 | return 106 | 107 | entry = self.storage[key] 108 | self.touch(entry) 109 | return entry.data[1] 110 | ``` 111 | 112 | The full code for the Python implementation, as well as a suite of tests, can be found [here][python-impl]. 113 | 114 | # Transitioning to a Rust Implementation 115 | 116 | Our Python implementation liberally allocates objects in memory, most notably the two distinct data structures, a dictionary and a doubly linked list (which itself allocates lots of linked list nodes), with linked list nodes referring to one another, along with the dictionary referring to these nodes as well. This is an example of what Jim Blandy and Jason Orendorff call a “sea of objects” in their book _Programming Rust_: 117 | 118 | ![Taking arms against a sea of objects](assets/sea-of-objects.png) 119 | 120 | Automatic memory management makes this kind of memory architecture tenable: cyclical references are handled by the garbage collector. The tradeoff here is that programmers are able to develop working software without having to do as much up-front planning, at the cost of taking a performance hit due to the garbage collection process. 121 | 122 | When it comes to implementing this same data structure in Rust, opting to go with the same high-level design of using a doubly linked list to keep track of the order of cache entries and then storing those linked list nodes as values in a dictionary for efficient access will lead to very messy and clunky code in the best case, and unsafe code in the worst case. Implementing a doubly linked list in Rust, at least in the “traditional” way, is, as we mentioned earlier, not straightforward. 123 | 124 | So we’ll need to go back to the drawing board and come up with a different architecture. Of the two data structures we used in our Python implementation, the doubly linked list is certainly the more vital of the two: it maintains the order of cache entries, is cheap to insert into and remove from, and provides us direct access to the most-recently- and least-recently-used entries in the cache so long as we maintain the invariant that these entries live at the ends of the linked list. 125 | 126 | But wait, didn’t we just mention that doubly linked lists are a pain to implement in Rust? 127 | 128 | Well, I said that they’re a pain to implement in Rust in the “traditional” way, using nodes and references between those nodes. The reason why is due to the ownership system that dictates that every heap-allocated memory object can only have one owner at any point in time. Doubly linked lists buck this rule since each node, by design, is referred to by two nodes at once. Thus, implementing a node-based doubly linked list either requires liberal use of `Rc`s (which impose runtime checks) or unsafety. 129 | 130 | But wait! There’s actually a third route we could take that circumvents these issues. We’ll implement our doubly linked list using an array 🙂 131 | 132 | Each “node” in our array-backed doubly linked list, instead of referencing other node objects directly, will instead refer to the array indices where the node’s previous and next nodes reside in the array. This is certainly a more abstract way to implement a doubly linked list, though it gets the job done and has some important benefits for our use case, most notably that introducing an extra level of indirection by having each linked list node reference the array index of another linked list node completely circumvents the multiple owners issue that comes with the territory of doubly linked lists. 133 | 134 | ![An array-backed doubly linked list](assets/array-backed-dll.png) 135 | 136 | Now, since all of our cache entries are being stored in an array, it would be straightforward to layer on a `HashMap` as part of our implementation where we store keys that are associated with a particular cache entry’s index in the array. 137 | 138 | However, instead of opting for this design, which would map relatively closely with the design of our Python implementation, we’re going to ditch the inclusion of a `HashMap` and actually just use our array-backed doubly linked list for our Rust implementation. This design decision means that we’re going to lose the ability to access arbitrary cache entries in constant time, but on the plus side, our cache will have a smaller memory footprint since we aren’t using another data structure. 139 | 140 | ## Implementing the Rust Version of our Cache 141 | 142 | Let’s start off by defining an `Entry` type inside the `src/lib.rs` file of a new Rust project. This type will represent every entry in our cache and wrap some arbitrary piece of data that we want stored in the cache. Note that this type also doubles as our linked list node type as it refers to the previous and next entries in the cache. 143 | 144 | ```rust 145 | pub struct Entry { 146 | /// The value stored in this entry. 147 | val: T, 148 | /// Index of the previous entry in the cache 149 | prev: usize, 150 | /// Index of the next entry in the cache 151 | next: usize, 152 | } 153 | ``` 154 | 155 | Next, our `LRUCache` type, which is responsible for storing an array of `Entry`s, as well as `head` and `tail` indices where the most-recently-used and least-recently-used `Entry`s are stored, respectively. The array will have a fixed size that can be specified when the `LRUCache` is initialized. Rust’s vanilla `array` [type][array] doesn’t actually satisfy this requirement ([yet][const-generics]), so we’ll need to import a crate that provides us with this capability, namely the `ArrayVec` [crate][arrayvec]. 156 | 157 | ### Bringing in `ArrayVec` as a Dependency 158 | 159 | First, add it as a dependency in your project’s `Cargo.toml` file: 160 | 161 | ``` 162 | [dependencies] 163 | arrayvec = “0.5.2” 164 | ``` 165 | 166 | Then in `src/lib.rs`: 167 | 168 | ```rust 169 | use arrayvec::ArrayVec; 170 | 171 | pub struct LRUCache { 172 | /// Our `ArrayVec` will be storing `Entry`s 173 | entries: ArrayVec, 174 | /// Index of the first entry in the cache 175 | head: usize, 176 | /// Index of the last entry in the cache 177 | tail: usize, 178 | /// The number of entries in the cache 179 | length: usize, 180 | } 181 | ``` 182 | 183 | If you try compiling what we have so far with `cargo build`, you should get an error like this: 184 | 185 | ``` 186 | error[E0107]: wrong number of type arguments: expected 1, found 0 187 | --> src/main.rs:11:23 188 | | 189 | 11 | entries: ArrayVec, 190 | | ^^^^^ expected 1 type argument 191 | 192 | error: aborting due to previous error 193 | ``` 194 | 195 | To figure out what’s wrong here, we can look at ArrayVec’s [documentation][arrayvec-docs]. One of the code snippets that demonstrates how to initialize a new `ArrayVec` instance looks like this: 196 | 197 | ```rust 198 | use arrayvec::ArrayVec; 199 | 200 | let mut array = ArrayVec::<[_; 16]>::new(); 201 | ``` 202 | 203 | The `<[_; 16]>` in this initialization is specifying up-front that the new `ArrayVec` will have a capacity of 16 and hold some unknown type (denoted by the `_`) that the compiler should infer later. This is the piece we’re missing in our code. But the whole point of bringing in and using `ArrayVec` was so that we could generalize over the capacity. How do we do that? 204 | 205 | If you search around `ArrayVec`’s docs, you might come across the `Array` [trait][array-trait]. This is what will allow us to generalize over the capacity of a new `ArrayVec` instance. When we go to initialize a new `LRUCache` instance, we can do so with something like this: 206 | 207 | ```rust 208 | // An `LRUCache` that holds at most 4 `Entry`s 209 | let cache = LRUCache<[Entry; 4]>; 210 | ``` 211 | 212 | You can think of the `Array` trait as a kind of placeholder that ensures that when it’s time to initialize an instance of our `LRUCache`, that the user specifies the type the cache will be holding and its capacity. 213 | 214 | We’ll change our code to use this trait: 215 | 216 | ```rust 217 | use arrayvec::{Array, ArrayVec}; // new import here 218 | 219 | // we’re saying here that any type our cache stores 220 | // must implement the `Array` trait 221 | pub struct LRUCache { 222 | entries: ArrayVec, // add the type parameter here 223 | head: usize, 224 | tail: usize, 225 | length: usize, 226 | } 227 | ``` 228 | 229 | With these changes in place, our code should compile successfully! 230 | 231 | ### Implementing Methods for our Cache 232 | 233 | We’ll start off with implementing a way to initialize a new `LRUCache` instance. The way we’ll opt to do this is by implementing the `Default` trait for our `LRUCache`. Normally, `Default` is implemented when you want to provide a method to initialize a type with a default set of field values or configuration. Here though, we’re going to have users of our cache always initialize an `LRUCache` instance by using the same syntax that `ArrayVec` uses. So you can think of it as we’re providing one default way to initialize an `LRUCache` instance. 234 | 235 | ```rust 236 | // the ‘default’ way to initialize an LRUCache instance 237 | // is by using specifying a type that implements the 238 | // `Array` trait, namely `<[type; capacity]>` 239 | impl Default for LRUCache { 240 | fn default() -> Self { 241 | let cache = LRUCache { 242 | entries: ArrayVec::new(), 243 | head: 0, 244 | tail: 0, 245 | length: 0, 246 | }; 247 | 248 | // check to make sure that the capacity provided by 249 | // the user is valid 250 | assert!( 251 | cache.entries.capacity() < usize::max_value(), 252 | “Capacity overflow” 253 | ); 254 | 255 | cache 256 | } 257 | } 258 | ``` 259 | 260 | ### Implementing `IterMut` 261 | 262 | Since we’ve decided that we aren’t going to use a `HashMap` in our implementation, we don’t have the capability to directly access any cache entries besides the head and tail entries. We’ll have to iterate through our list of entries to find a particular value to fetch from our cache. On that note, it behooves us to implement the `Iterator` trait for our cache. 263 | 264 | We could opt to implement both mutable and immutable iteration, but, thinking about the use case of our cache, it won’t be uncommon for users to want to mutate cache entries. So we’ll just take the lazy route and only implement an iterator that hands out mutable references to cache entries. 265 | 266 | The first thing we need to do is define a type that keeps track of the state of the iterator. Most importantly, the position of where the iterator is at any point in time: 267 | 268 | ```rust 269 | // keeps track of where we currently are over 270 | // the course of iteration 271 | struct IterMut<‘a, A: ‘a + Array> { 272 | cache: &’a mut LRUCache, 273 | pos: usize, 274 | done: bool, 275 | } 276 | ``` 277 | 278 | Our `IterMut` struct needs a mutable reference to our cache itself so that mutable references can be handed out from it. This mutable reference to the cache needs to be valid for at least as long as the `LRUCache` itself is valid, which is what the `‘a` lifetime is specifying. 279 | 280 | Starting off the `Iterator` implementation for our `IterMut` type, we continue to use the `’a` lifetime to specify that `T` (which the type that is being stored in our `Entry`s) and the underlying `Array` type that our `LRUCache` wraps both live at least as long as our `IterMut` type. 281 | 282 | ```rust 283 | impl<‘a, T, A> Iterator for IterMut<‘a, A> 284 | where 285 | T: ‘a, 286 | A: ‘a + Array>, 287 | { 288 | type Item = (usize, &’a mut T); 289 | } 290 | ``` 291 | 292 | The `type Item = (usize, &’a mut T);` line indicates that each iteration yields a tuple with the position of the yielded entry, and a mutable reference to the underlying type that the entry was wrapping. 293 | 294 | To complete our `IterMut` implementation, the only method we need to implement is a `next` method that either returns an `Item` or `None` if there are no more items to be yielded from the iteration: 295 | 296 | ```rust 297 | fn next(&mut self) -> Option { 298 | // check if we’ve iterated through all entries 299 | if self.done { 300 | return None; 301 | } 302 | 303 | // get the current entry and index 304 | let entry = self.cache.entries[self.pos]; 305 | let index = self.pos; 306 | 307 | // we’re done iterating once we reach the tail entry 308 | if self.pos == self.cache.tail { 309 | self.done = true; 310 | } 311 | 312 | // increment our position 313 | self.pos = entry.next; 314 | 315 | Some((index, &mut entry.val)) 316 | } 317 | ``` 318 | 319 | However, if you try to compile this code, you’ll get the following errors: 320 | 321 | ``` 322 | error[E0508]: cannot move out of type `[Entry]`, a non-copy slice 323 | --> src/lib.rs:258:21 324 | | 325 | 258 | let entry = self.cache.entries[self.pos]; 326 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 327 | | | 328 | | cannot move out of here 329 | | move occurs because value has type `Entry`, which does not implement the `Copy` trait 330 | | help: consider borrowing here: `&self.cache.entries[self.pos]` 331 | 332 | error[E0515]: cannot return value referencing local data `entry.val` 333 | --> src/lib.rs:268:9 334 | | 335 | 268 | Some((index, &mut entry.val)) 336 | | ^^^^^^^^^^^^^--------------^^ 337 | | | | 338 | | | `entry.val` is borrowed here 339 | | returns a value referencing data owned by the current function 340 | 341 | error[E0596]: cannot borrow `entry.val` as mutable, as `entry` is not declared as mutable 342 | --> src/lib.rs:268:22 343 | | 344 | 258 | let entry = self.cache.entries[self.pos]; 345 | | ----- help: consider changing this to be mutable: `mut entry` 346 | ... 347 | 268 | Some((index, &mut entry.val)) 348 | | ^^^^^^^^^^^^^^ cannot borrow as mutable 349 | 350 | error: aborting due to 3 previous errors 351 | 352 | ``` 353 | 354 | Here, we’re getting a compiler error because our code is saying that our `IterMut` type is taking ownership of the cache entry, which would mean that the entry is no longer owned by the cache itself! The effect of this, if the compiler allowed us to follow through with it, is that iterating through all of the cache’s entries would move all of those entries out of the cache, leaving the cache with no entries after a single iteration pass! That’s... certainly not what we want. 355 | 356 | Let’s try changing the line this to take a mutable reference to the current entry instead, which is what our `IterMut` type is looking to do anyway: 357 | 358 | ```rust 359 | let entry = &mut self.cache.entries[self.pos]; 360 | ``` 361 | 362 | This change yields a different error: 363 | 364 | ``` 365 | error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements 366 | --> src/lib.rs:258:26 367 | | 368 | 258 | let entry = &mut self.cache.entries[self.pos]; 369 | | ^^^^^^^^^^^^^^^^^^ 370 | | 371 | ``` 372 | 373 | The compiler cannot adequately prove that these borrows to each cache entry will not live longer than the cache itself. The caller of the `iter_mut` method is free to do whatever they wish with the mutable references that are handed out. 374 | 375 | Moreover, the compiler also cannot prove that subsequent `next` calls won’t see us handing out a second mutable reference to an entry that already has a mutable reference referring to it. However, this is a consequence of the compiler not being smart enough to figure this out on its own. We, the programmer, know that subsequent `next` calls will advance the iterator forward to the next entry, so there will always exist at most one mutable reference to each cache entry at a time. 376 | 377 | In order to resolve this, we’re going to tell the compiler to trust us and opt for the backdoor option of dipping into some unsafe code here. We’ll take a raw pointer to each cache entry, which is essentially us telling the compiler that whatever this raw pointer refers to will not cause undefined behavior, because we know that it won’t, so long as we use `IterMut` in the intended way. 378 | 379 | ```rust 380 | let entry = unsafe { &mut *(&mut self.cache.entries[self.pos] as *mut Entry) }; 381 | ``` 382 | 383 | If this makes you uneasy (and honestly, it probably should at least a little bit), well, you can at least take some solace in that fact that this isn’t production code that has any possibility of causing actual problems of consequence down the line 🙂 384 | 385 | But with that change, our code should compile without issue! 386 | 387 | ### Implementing Actual Cache Functionality 388 | 389 | We’ll start off by getting the easy methods out of the way: 390 | 391 | ```rust 392 | // we need to bring the types that our `LRUCache` is 393 | // parameterized over into our `impl` block 394 | impl LRUCache 395 | where 396 | A: Array>, 397 | { 398 | /// Returns the number of entries in the cache 399 | pub fn len(&self) -> usize { 400 | self.length 401 | } 402 | 403 | /// Indicates whether the cache is empty or not 404 | pub fn is_empty(&self) -> bool { 405 | self.length == 0 406 | } 407 | 408 | /// Returns an instance of our `IterMut` type 409 | /// We’ll keep this function private to minimize 410 | /// the chance of mutable references to cache 411 | /// entries being used incorrectly 412 | fn iter_mut(&mut self) -> IterMut { 413 | IterMut { 414 | pos: self.head, 415 | done: self.is_empty(), 416 | cache: self, 417 | } 418 | } 419 | 420 | /// Clears the cache of all entries 421 | pub fn clear(&mut self) { 422 | self.entries.clear(); 423 | self.head = 0; 424 | self.tail = 0; 425 | self.length = 0; 426 | } 427 | } 428 | ``` 429 | 430 | It will be convenient to add some methods for manipulating elements in our linked list, such as being able to add an entry to the head of the list or to the tail of the list. Let’s add those: 431 | 432 | ```rust 433 | /// Returns a reference to the element stored at 434 | /// the head of the list 435 | pub fn front(&self) -> Option&T> { 436 | // fetch the head entry and return a 437 | // reference to the inner value 438 | self.entries.get(self.head).map(|e| &e.val) 439 | } 440 | 441 | /// Returns a mutable reference to the element stored 442 | // at the head of the list 443 | pub fn front_mut(&mut self) -> Option<&mut T> { 444 | // fetch the head entry mutably and return a 445 | // mutable reference to the inner value 446 | self.entries.get_mut(self.head).map(|e| &mut e.val) 447 | } 448 | 449 | /// Takes an entry that has been added to the linked 450 | /// list and moves the head to the entry’s position 451 | fn push_front(&mut self, index: usize) { 452 | if self.entries.len() == 1 { 453 | self.tail = index; 454 | } else { 455 | self.entries[index].next = self.head; 456 | self.entries[self.head].prev = index; 457 | } 458 | 459 | self.head = index; 460 | } 461 | 462 | /// Remove the last entry from the list and returns 463 | /// the index of the removed entry. Note that this 464 | /// only unlinks the entry from the list, it doesn’t 465 | /// remove it from the array. 466 | fn pop_back(&mut self) -> usize { 467 | let old_tail = self.tail; 468 | let new_tail = self.entries[old_tail].prev; 469 | self.tail = new_tail; 470 | old_tail 471 | } 472 | ``` 473 | 474 | We’ll add one more method, `remove`, that takes as input an index into our array-backed linked list and “removes” the entry at that index. Note that this method actually only unlinks the entry from the linked list without actually removing it from the array. This is to avoid the runtime overhead of having to shift subsequent array elements forward to fill in the empty slot. 475 | 476 | ```rust 477 | fn remove(&mut self, index: usize) { 478 | assert!(self.length > 0); 479 | 480 | let prev = self.entries[index].prev; 481 | let next = self.entries[index].next; 482 | 483 | if index == self.head { 484 | self.head = next; 485 | } else { 486 | self.entries[prev].next = next; 487 | } 488 | 489 | if index == self.tail { 490 | self.tail = prev; 491 | } else { 492 | self.entries[next].prev = prev; 493 | } 494 | 495 | self.length -= 1; 496 | } 497 | ``` 498 | 499 | ### Touching our Entries 500 | 501 | With those out of the way, we can now implement the functionality that makes up the main attraction. We’ll start with the `touch` method, which is responsible for finding an entry and moving it from its initial position in the cache to the head of the linked list. 502 | 503 | In our Python implementation, we could conveniently access any entry in the cache via its associated key in the dictionary. Here, we opted to forgo that convenience. We’ll have to iterate over each entry and determine if it’s the entry we’re looking to move; only then will we have the index of the entry and can actually move it. 504 | 505 | Let’s first define a helper method called `touch_index` that will receive an index to an entry in our cache and move it to the head of the linked list: 506 | 507 | ```rust 508 | /// Touch a given entry at the given index, putting it 509 | /// first in the list. 510 | fn touch_index(&mut self, index: usize) { 511 | if index != self.head { 512 | self.remove(index); 513 | 514 | // need to increment `self.length` here since 515 | // `remove` decrements it 516 | self.length += 1; 517 | self.push_front(index); 518 | } 519 | } 520 | ``` 521 | 522 | With that, we can now implement `touch`. We’ll specify that the predicate must implement the `FnMut` trait, which, according to the [docs][fnmut-docs], fits our use-case nicely: 523 | 524 | > Use `FnMut` as a bound when you want to accept a parameter of function-like type and need to call it repeatedly, while allowing it to mutate state. 525 | 526 | We’re more concerned with being able to “call it [the predicate] repeatedly” rather than having it mutate state (which the predicate shouldn’t be doing). 527 | 528 | Our `touch` method iterates over the entries in our cache (using our `iter_mut` method) and finds the first entry whose value matches the predicate. We then pass the index associated with the found entry to our `touch_index` method, which handles moving the entry to the head of the list: 529 | 530 | ```rust 531 | /// Touches the first entry in the cache that matches the 532 | /// given predicate. Returns `true` on a hit and `false` 533 | /// if no match is found. 534 | pub fn touch(&mut self, mut pred: F) -> bool 535 | where 536 | F: FnMut(&T) -> bool, 537 | { 538 | match self.iter_mut().find(|&(_, ref x)| pred(x)) { 539 | Some((i, _)) => { 540 | self.touch_index(i); 541 | true 542 | }, 543 | None => false, 544 | } 545 | } 546 | ``` 547 | 548 | We can implement a similar method, `lookup`, which will essentially do the same thing as what our `touch` method is doing, but instead of returning a boolean indicating whether an entry matching the input predicate was found or not, `lookup` instead returns the found entry’s value or `None`. 549 | 550 | ```rust 551 | pub fn lookup(&mut self, mut pred: F) -> Option 552 | where 553 | F: FnMut(&mut T) -> Option, 554 | { 555 | let mut result = None; 556 | 557 | // iterate through our entries, testing each 558 | // using the predicate 559 | for (i, entry) in self.iter_mut() { 560 | if let Some(r) = pred(entry) { 561 | result = Some((i, r)); 562 | break; 563 | } 564 | } 565 | 566 | // once we’ve iterated through all entries, match 567 | // on the result to move it to the head of the list 568 | // if necessary 569 | match result { 570 | None => None, 571 | Some((i, r)) => { 572 | self.touch_index(i); 573 | Some(r) 574 | } 575 | } 576 | } 577 | ``` 578 | 579 | ### Inserting New Entries 580 | 581 | We can now fetch existing entries from our cache, and those entries will be moved to the head of the list when they are fetched. Let’s add an `insert` method that will take some `val` of arbitrary type and add it to the head of the linked list: 582 | 583 | ```rust 584 | fn insert(&mut self, val: T) { 585 | let entry = Entry { 586 | val, 587 | prev: 0, 588 | next: 0, 589 | }; 590 | 591 | // check if the cache is at full capacity 592 | let new_head = if self.length == self.entries.capacity() { 593 | // get the index of the oldest entry 594 | let last_index = self.pop_back(); 595 | // overwrite the oldest entry with the new entry 596 | self.entries[last_index] = entry; 597 | // return the index of the newly-overwritten entry 598 | last_index 599 | } else { 600 | self.entries.push(entry); 601 | self.length += 1; 602 | self.entries.len() - 1 603 | }; 604 | 605 | self.push_front(new_head); 606 | } 607 | ``` 608 | 609 | With that, our cache is feature-complete! You can find the full code [here][rust-impl], complete with tests as well as some additional miscellaneous trait implementations. 610 | 611 | # In Summary 612 | 613 | I had a lot of fun transposing a Python data structure implementation that I’m quite familiar with into a Rust version. Clearly, the fact that Python and Rust belong in such different paradigms contributes significantly to how these implementations turned out. 614 | 615 | However, we did inject some of our own design decisions into the Rust implementation as well (opting to forgo including a HashMap as part of the design), and that too contributed in part to the data structure’s design being so different between the two languages. 616 | 617 | I hope you had fun learning about LRU caches as well! 🙂 618 | 619 | [array]: https://docs.rust-lang.org/std/primitive.array.html 620 | [array-trait]: https://docs.rs/arrayvec/0.5.2/arrayvec/trait.Array.html 621 | [arrayvec]: https://docs.rs/arrayvec/0.5.2/arrayvec/ 622 | [arrayvec-docs]: https://docs.rs/arrayvec/0.5.2/arrayvec/struct.ArrayVec.html 623 | [const-generics]: https://doc.rust-lang.org/nightly/reference/items/generics.html#const-generics 624 | [fnmut-docs]: https://doc.rust-lang.org/std/ops/trait.FnMut.html 625 | [python-impl]: https://github.com/seanchen1991/python-lru/blob/main/src/lru_cache.py 626 | [rust-impl]: https://github.com/seanchen1991/rust-data-structures/blob/master/lru/src/lib.rs 627 | [traits-chapter]: https://docs.rust-lang.org/book/ch10-02-traits.html 628 | [too-many-lists]: https://rust-unofficial.github.io/too-many-lists/fourth.html 629 | --------------------------------------------------------------------------------