exercises

Log | Files | Refs | README

commit e1fe241540b76fbce2066c66478c5bd78fd9cf04
Author: ling0x <ling0x@users.noreply.github.com>
Date:   Mon,  8 Jun 2026 03:05:59 +0100

new commit

Diffstat:
A.gitignore | 4++++
Aactors/Cargo.lock | 218+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aactors/Cargo.toml | 7+++++++
Aactors/index.md | 13+++++++++++++
Aactors/src/main.rs | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achannels/Cargo.lock | 7+++++++
Achannels/Cargo.toml | 6++++++
Achannels/src/main.rs | 3+++
Adesign_patterns_in_rust/bad_calculator/Cargo.lock | 7+++++++
Adesign_patterns_in_rust/bad_calculator/Cargo.toml | 6++++++
Adesign_patterns_in_rust/bad_calculator/src/main.rs | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_2/Cargo.lock | 7+++++++
Adesign_patterns_in_rust/bad_calculator_2/Cargo.toml | 6++++++
Adesign_patterns_in_rust/bad_calculator_2/src/cloning_bad_example.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_2/src/cloning_better_example.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_2/src/lib.rs | 19+++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_2/src/main.rs | 27+++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_2/src/ownership_bad_example.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_2/src/ownership_better_example.rs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_2/src/smart_pointers_bad_example.rs | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_2/src/smart_pointers_better_example.rs | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_3/Cargo.lock | 16++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_3/Cargo.toml | 7+++++++
Adesign_patterns_in_rust/bad_calculator_3/src/bad_example.rs | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_3/src/bad_example_statics.rs | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_3/src/bad_example_unsafe.rs | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_3/src/better_example.rs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_3/src/better_example_no_statics.rs | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_3/src/better_example_no_unsafe.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/bad_calculator_3/src/lib.rs | 6++++++
Adesign_patterns_in_rust/bad_calculator_3/src/main.rs | 17+++++++++++++++++
Adesign_patterns_in_rust/good_calculator/Cargo.lock | 7+++++++
Adesign_patterns_in_rust/good_calculator/Cargo.toml | 6++++++
Adesign_patterns_in_rust/good_calculator/src/builder.rs | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator/src/calculator.rs | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator/src/factory.rs | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator/src/lib.rs | 28++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator/src/main.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator/src/number.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator/src/operator.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator/src/token.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/Cargo.lock | 7+++++++
Adesign_patterns_in_rust/good_calculator_2/Cargo.toml | 6++++++
Adesign_patterns_in_rust/good_calculator_2/src/adapters.rs | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/bridge_pattern.rs | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/builder.rs | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/calculator.rs | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/facade.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/factory.rs | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/lib.rs | 44++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/main.rs | 43+++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/number.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/operator.rs | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/token.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/good_calculator_2/src/utilities.rs | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/index.md | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/not_so_bad_calculator/Cargo.lock | 7+++++++
Adesign_patterns_in_rust/not_so_bad_calculator/Cargo.toml | 6++++++
Adesign_patterns_in_rust/not_so_bad_calculator/src/main.rs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/slightly_worse_calculator/Cargo.lock | 7+++++++
Adesign_patterns_in_rust/slightly_worse_calculator/Cargo.toml | 6++++++
Adesign_patterns_in_rust/slightly_worse_calculator/src/main.rs | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign_patterns_in_rust/the_worst_calculator/Cargo.lock | 7+++++++
Adesign_patterns_in_rust/the_worst_calculator/Cargo.toml | 6++++++
Adesign_patterns_in_rust/the_worst_calculator/src/main.rs | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aindex.md | 5+++++
Amy_vec/Cargo.lock | 7+++++++
Amy_vec/Cargo.toml | 6++++++
Amy_vec/index.md | 9+++++++++
Amy_vec/src/main.rs | 35+++++++++++++++++++++++++++++++++++
Aspinlock/Cargo.lock | 7+++++++
Aspinlock/Cargo.toml | 6++++++
Aspinlock/index.md | 11+++++++++++
Aspinlock/src/main.rs | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atokio-tutorial/index.md | 11+++++++++++
Atokio-tutorial/my-redis/Cargo.lock | 1014+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atokio-tutorial/my-redis/Cargo.toml | 9+++++++++
Atokio-tutorial/my-redis/examples/hello-redis.rs | 23+++++++++++++++++++++++
Atokio-tutorial/my-redis/index.md | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atokio-tutorial/my-redis/src/bin/client.rs | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atokio-tutorial/my-redis/src/bin/server.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atokio-tutorial/my-redis/src/connection.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atokio-tutorial/my-redis/src/frame.rs | 0
Atokio-tutorial/my-redis/src/lib.rs | 2++
84 files changed, 5827 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +node_modules +public +target/ +\ No newline at end of file diff --git a/actors/Cargo.lock b/actors/Cargo.lock @@ -0,0 +1,218 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actors" +version = "0.1.0" +dependencies = [ + "tokio", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/actors/Cargo.toml b/actors/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "actors" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { version = "1.50.0", features = ["full"] } diff --git a/actors/index.md b/actors/index.md @@ -0,0 +1,13 @@ +--- +title: "Exercise: Actors" +tags: [exercise, rust, concurrency, memory-safety] +--- + +A from-scratch implementation of an +[actor](/async_programming/actors.md) in Rust using tokio pmsc +channels. The goal is to understand how an actor works at the lowest level — +without relying on any framework following +[Alice Rhyl's blog post](https://ryhl.io/blog/actors-with-tokio/). + +> [!info] Source Code: +> [exercises/actors/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/actors/src/main.rs) diff --git a/actors/src/main.rs b/actors/src/main.rs @@ -0,0 +1,146 @@ +//! An exercise to create a simple actor +//! +//! What is an actor? +//! +//! The basic idea behind an actor is to spawn a self-contained task that performs +//! some job independently of other parts of the program. +//! +//! Typically these actors communicate with the rest of the program through the +//! use of message passing channels. +//! +//! Since each actor runs independently, programs designed using them are +//! naturally parallel. +//! +//! Things to pay attention to: +//! 1. Where to put the tokio::spawn call. +//! 2. Struct with run method vs bare function. +//! 3. andles to the actor. +//! 4. Backpressure and bounded channels. +//! 5. Graceful shutdown. + +use tokio::sync::{mpsc, oneshot}; + +/// An actor is split into two parts: the task and the handle. +/// +/// The task is the independently spawned Tokio task that actually performs +/// the duties of the actor, and the handle is a struct that allows you to +/// communicate with the task. +struct MyActor { + receiver: mpsc::Receiver<ActorMessage>, + next_id: u32, +} + +/// The ActorMessage enum defines the kind of messages we can send to the actor. +/// +/// By using an enum, we can have many different message types, and each message +/// type can have its own set of arguments. We return a value to the sender by +/// using an oneshot channel, which is a message passing channel that allows +/// sending exactly one message. +enum ActorMessage { + GetUniqueId { respond_to: oneshot::Sender<u32> }, +} + +impl MyActor { + fn new(receiver: mpsc::Receiver<ActorMessage>) -> Self { + MyActor { + receiver, + next_id: 0, + } + } + + fn handle_message(&mut self, msg: ActorMessage) { + // We match on the enum inside a handle_message method on the actor struct, + // but that isn't the only way to structure this. One could also match on + // the enum in the run_my_actor function. Each branch in this match could + // then call various methods such as get_unique_id on the actor object. + match msg { + ActorMessage::GetUniqueId { respond_to } => { + self.next_id += 1; + + // The `let _ =` ignores any errors when sending. + // + // This can happen if the `select!` macro is used + // to cancel waiting for the response. + let _ = respond_to.send(self.next_id); + } + } + } +} + +async fn run_my_actor(mut actor: MyActor) { + // We can detect when the actor should shut down by looking at failures to + // receive messages. In our example, this happens in the following while loop: + while let Some(msg) = actor.receiver.recv().await { + actor.handle_message(msg); + } +} + +/// Now that we have the actor itself, we also need a handle to the actor. +/// +/// A handle is an object that other pieces of code can use to talk to the actor, +/// and is also what keeps the actor alive. +/// +/// Derive Clone: Since the channel allows multiple producers, we can freely +/// clone our handle to the actor, allowing us to talk to it from multiple places. +#[derive(Clone)] +pub struct MyActorHandle { + sender: mpsc::Sender<ActorMessage>, +} + +impl MyActorHandle { + pub fn new() -> Self { + let (sender, receiver) = mpsc::channel(8); + let actor = MyActor::new(receiver); + tokio::spawn(run_my_actor(actor)); + + Self { sender } + } + + pub async fn get_unique_id(&self) -> u32 { + let (send, recv) = oneshot::channel(); + let msg = ActorMessage::GetUniqueId { respond_to: send }; + + // Ignore send errors. If this send fails, so does the + // recv.await below. There's no reason to check for the + // same failure twice. + let _ = self.sender.send(msg).await; + recv.await.expect("Actor task has been killed") + } +} + +/// When you call MyActorHandle::new(), it already calls tokio::spawn(run_my_actor(actor)) +/// internally. So by the time new() returns, the actor task is live and waiting +/// for messages on its mpsc::Receiver. +/// +/// You never call tokio::spawn in main — it's encapsulated inside MyActorHandle::new(), +/// which is the idiomatic placement for this pattern. This keeps the spawning +/// logic close to the actor itself. +/// +/// Graceful shutdown is automatic — when actor_handle (and all its clones) are +/// dropped, the mpsc::Sender is dropped, causing actor.receiver.recv().await to +/// return None, breaking the while let loop and ending the task. +/// +/// Cloning the handle is safe — MyActorHandle derives Clone, so multiple parts +/// of your program can send messages to the same actor concurrently without any +/// extra synchronization, since the actor processes them one at a time. +/// +/// Backpressure is built in — the channel is bounded (mpsc::channel(8)), so if +/// the actor can't keep up, senders will .await until there's room, naturally +/// throttling the workload. +#[tokio::main] +async fn main() { + // 1. Creating the handle also spawns the actor task automatically (inside MyActorHandle::new) + let actor_handle = MyActorHandle::new(); + + // 2. Send a message to the actor and await the response + let id1 = actor_handle.get_unique_id().await; + println!("Got id: {}", id1); // prints 1 + + let id2 = actor_handle.get_unique_id().await; + println!("Got id: {}", id2); // prints 2 + + // 3. Clone the handle to show multiple owners can talk to the same actor + let handle2 = actor_handle.clone(); + let id3 = handle2.get_unique_id().await; + println!("Got id from cloned handle: {}", id3); // prints 3 +} diff --git a/channels/Cargo.lock b/channels/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "channels" +version = "0.1.0" diff --git a/channels/Cargo.toml b/channels/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "channels" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/channels/src/main.rs b/channels/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/design_patterns_in_rust/bad_calculator/Cargo.lock b/design_patterns_in_rust/bad_calculator/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bad_calculator" +version = "0.1.0" diff --git a/design_patterns_in_rust/bad_calculator/Cargo.toml b/design_patterns_in_rust/bad_calculator/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "bad_calculator" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/design_patterns_in_rust/bad_calculator/src/main.rs b/design_patterns_in_rust/bad_calculator/src/main.rs @@ -0,0 +1,167 @@ +//! This is a bad example of trying to apply OO principles in rust +//! and make it worse + +use std::{ + io::{Write, stdin, stdout}, + ops::{Deref, DerefMut}, + process::exit, +}; + +trait Operand { + fn evaluate(&self) -> f64; +} + +/// There are a lot of box dyn types showing up, which is bit +/// concerning, but lets continue to make the bad calculator +trait Operator { + fn precedence(&self) -> u8; + fn symbol(&self) -> char; + fn push_operand(&mut self, operand: Box<dyn Operand>); + fn pop_operand(&mut self) -> Box<dyn Operand>; + fn apply(&mut self) -> Box<dyn Operand>; +} + +trait UnaryOperator: crate::Operator { + fn apply_unary(&self, operand: Box<dyn Operand>) -> Box<dyn Operand>; + + fn apply(&mut self) -> Box<dyn Operand> { + let operand = self.pop_operand(); + self.apply_unary(operand) + } +} + +trait BinaryOperator: Operator { + fn apply_binary( + &self, + operand1: Box<dyn Operand>, + operand2: Box<dyn Operand>, + ) -> Box<dyn Operand>; + + fn apply(&mut self) -> Box<dyn Operand> { + let operand2 = self.pop_operand(); + let operand1 = self.pop_operand(); + self.apply_binary(operand1, operand2) + } +} + +struct OperandStack(Vec<Box<dyn Operand>>); + +impl OperandStack { + fn new() -> Self { + Self(Vec::new()) + } + + fn push_operand(&mut self, operand: Box<dyn Operand>) { + self.0.push(operand); + } + + fn pop_operand(&mut self) -> Box<dyn Operand> { + self.0.pop().unwrap() + } + + fn clear_stack(&mut self) { + self.0.clear(); + } +} + +struct AdditionOperator { + stack: OperandStack, +} + +impl AdditionOperator { + fn new() -> Self { + Self { + stack: OperandStack::new(), + } + } +} + +impl BinaryOperator for AdditionOperator { + fn apply_binary( + &self, + operand1: Box<dyn Operand>, + operand2: Box<dyn Operand>, + ) -> Box<dyn Operand> { + let inner_operand2 = operand2.as_ref().evaluate(); + let inner_operand1 = operand1.evaluate(); + let result = inner_operand1 + inner_operand2; + Box::new(Value(result)) + } +} + +impl Operator for AdditionOperator { + fn precedence(&self) -> u8 { + 0 + } + + fn symbol(&self) -> char { + '+' + } + + fn push_operand(&mut self, operand: Box<dyn Operand>) { + self.stack.push_operand(operand); + } + + fn pop_operand(&mut self) -> Box<dyn Operand> { + self.stack.pop_operand() + } + + fn apply(&mut self) -> Box<dyn Operand> { + let operand2 = self.pop_operand(); + let operand1 = self.pop_operand(); + self.apply_binary(operand1, operand2) + } +} + +impl Deref for AdditionOperator { + type Target = OperandStack; + + fn deref(&self) -> &Self::Target { + &self.stack + } +} + +impl DerefMut for AdditionOperator { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.stack + } +} + +struct Value(f64); + +impl Operand for Value { + fn evaluate(&self) -> f64 { + self.0 + } +} + +fn evaluate_expression(expression: &str) -> Result<String, String> { + let some_operand = Box::new(Value(0.0)); + let mut addition_operator = AdditionOperator::new(); + addition_operator.push_operand(some_operand); + addition_operator.clear_stack(); + let popped_operand = addition_operator.pop_operand(); + Ok("Finished".to_string()) +} + +/// This is a project to demonstrate BAD practices in Rust in order to learn +fn main() { + let mut buf = String::new(); + loop { + print!("> "); + + stdout().flush().unwrap(); + + buf.clear(); + stdin().read_line(&mut buf).unwrap(); + + if buf.trim() == "exit" { + exit(0) + } + + match evaluate_expression(&buf) { + Ok(result) => println!("{result}"), + Err(error) => println!("Error: {error}"), + } + } +} diff --git a/design_patterns_in_rust/bad_calculator_2/Cargo.lock b/design_patterns_in_rust/bad_calculator_2/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bad_calculator_2" +version = "0.1.0" diff --git a/design_patterns_in_rust/bad_calculator_2/Cargo.toml b/design_patterns_in_rust/bad_calculator_2/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "bad_calculator_2" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/design_patterns_in_rust/bad_calculator_2/src/cloning_bad_example.rs b/design_patterns_in_rust/bad_calculator_2/src/cloning_bad_example.rs @@ -0,0 +1,71 @@ +//! This code is a bad example that is riddled with unecessary cloning + +use std::collections::HashMap; + +/// Notice `#[derive(Clone)]` on both structs, this is our first warning. +/// Do we really need to derive it on everything? +#[derive(Clone)] +struct Variable { + name: String, + value: f64, +} + +/// Another derive Clone :/ +#[derive(Clone)] +enum Token { + Number(f64), + Variable(Variable), + Operator(char), +} + +struct Calculator { + variables: HashMap<String, Variable>, +} + +/// Bad practice: use clone() everywhere... +impl Calculator { + fn tokenize(&self, expression: &str) -> Vec<Token> { + let mut tokens = Vec::new(); + + for part in expression.split_whitespace() { + if let Some(var) = self.variables.get(part) { + // Another clone() :] + tokens.push(Token::Variable(var.clone())); + } + // ... rest of tokenization + } + tokens + } + + fn apply_operator(&self, left: Token, right: Token) -> f64 { + todo!() + } + + fn evaluate(&self, tokens: Vec<Token>) -> f64 { + // Yet another clone... + let mut working_tokens = tokens.clone(); + + while working_tokens.len() > 1 { + // Find next operator + let op_pos = working_tokens + .iter() + .position(|t| matches!(t, Token::Operator(_))) + .unwrap(); + + // So many clones :/ + let left = working_tokens[op_pos - 1].clone(); + let right = working_tokens[op_pos + 1].clone(); + + let result = self.apply_operator(left, right); + + // Remove old tokens and insert result + working_tokens.drain(op_pos - 1..=op_pos + 1); + working_tokens.insert(op_pos - 1, Token::Number(result)); + } + + match working_tokens[0] { + Token::Number(n) => n, + _ => panic!("Invalid expression"), + } + } +} diff --git a/design_patterns_in_rust/bad_calculator_2/src/cloning_better_example.rs b/design_patterns_in_rust/bad_calculator_2/src/cloning_better_example.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; + +struct Variable { + name: String, + value: f64, +} + +/// Notice that Token now holds a reference to Variable instead of owning a clone +/// The 'a lifetime parameter ties the token's validity to the varaible it references +enum Token<'a> { + Number(f64), + Variable(&'a Variable), + Operator(char), +} + +struct Calculator { + variables: HashMap<String, Variable>, +} + +impl Calculator { + /// The methods now work with references instead of clones + fn tokenize<'a>(&'a self, expression: &str) -> Vec<Token<'a>> { + let mut tokens = Vec::new(); + + for part in expression.split_whitespace() { + if let Some(var) = self.variables.get(part) { + // No need to clone, just use a reference + tokens.push(Token::Variable(var)); + } + // ... rest of tokenization + } + tokens + } + + fn apply_operator(&self, left: f64, right: f64) -> f64 { + todo!() + } + + fn evaluate(&self, mut tokens: Vec<Token>) -> f64 { + while tokens.len() > 1 { + // Find next operator + let op_pos = tokens + .iter() + .position(|t| matches!(t, Token::Operator(_))) + .unwrap(); + + // Calculate the result using references + let result = match (&tokens[op_pos - 1], &tokens[op_pos + 1]) { + (Token::Number(n1), Token::Number(n2)) => self.apply_operator(*n1, *n2), + (Token::Variable(v1), Token::Number(n2)) => self.apply_operator(v1.value, *n2), + // ... other combinations + _ => panic!("Invalid expression"), + }; + + // Remove old tokens and insert result + tokens.drain(op_pos - 1..=op_pos + 1); + tokens.insert(op_pos - 1, Token::Number(result)); + } + + match tokens[0] { + Token::Number(n) => n, + _ => panic!("Invalid expression"), + } + } +} diff --git a/design_patterns_in_rust/bad_calculator_2/src/lib.rs b/design_patterns_in_rust/bad_calculator_2/src/lib.rs @@ -0,0 +1,19 @@ +/// Example 1: fighting the borrow check with refcell +pub mod ownership_bad_example; + +/// Example 2: make it work within the ownership rules +pub mod ownership_better_example; + +/// Example 3: cloning - using clone() everywhere as a clone hammer +pub mod cloning_bad_example; + +/// Example 4: cloning - work within the ownership and lifetime rules +pub mod cloning_better_example; + +/// Example 5: smart pointers - over-engineer with smart pointers and wrap +/// everything in Rc<RefCell<...>> +pub mod smart_pointers_bad_example; + +/// Example 6: smart pointers - design a system by thinking about ownership +/// and data flow upfront +pub mod smart_pointers_better_example; diff --git a/design_patterns_in_rust/bad_calculator_2/src/main.rs b/design_patterns_in_rust/bad_calculator_2/src/main.rs @@ -0,0 +1,27 @@ +use std::{fmt::Pointer, thread::spawn}; + +use bad_calculator_2::smart_pointers_better_example::{Calculator, ThreadSafeCalculator}; + +fn main() -> Result<(), String> { + let mut calc = Calculator::new(); + + calc.set_variable("pi".to_string(), 3.14159); + + let result1 = calc.evaluate("2 * pi".to_string())?; + let result2 = calc.evaluate("result + 1".to_string())?; + + for calculation in calc.history() { + println!("{} = {}", calculation.expression, calculation.result); + } + + let thread_safe = ThreadSafeCalculator::new(); + + let threads: Vec<_> = (0..3) + .map(|i| { + let calc = thread_safe.clone(); + spawn(move || calc.evaluate(format!("{} + 1", i))) + }) + .collect(); + + Ok(()) +} diff --git a/design_patterns_in_rust/bad_calculator_2/src/ownership_bad_example.rs b/design_patterns_in_rust/bad_calculator_2/src/ownership_bad_example.rs @@ -0,0 +1,65 @@ +use std::cell::{Ref, RefCell}; + +struct CalculationResult { + expression: String, + result: f64, +} + +/// Anti-pattern: trying to circumvent the borrow checker with RefCell +struct Calculator { + history: RefCell<Vec<CalculationResult>>, + current_expression: RefCell<Option<String>>, +} + +trait HistoryViewer { + fn view_history(&self) -> Ref<Vec<CalculationResult>>; + fn get_last_result(&self) -> Option<f64>; +} + +trait HistoryManager { + fn add_to_history(&self, expression: String, result: f64); + fn clear_history(&self); +} + +impl Calculator { + fn new() -> Self { + Self { + history: RefCell::new(Vec::new()), + current_expression: RefCell::new(None), + } + } +} + +/// Implementing HistoryViewer works fine, as we can reference the data with &self +impl HistoryViewer for Calculator { + fn view_history(&self) -> Ref<Vec<CalculationResult>> { + self.history.borrow() + } + + fn get_last_result(&self) -> Option<f64> { + self.history.borrow().last().map(|r| r.result) + } +} + +/// However, mutable access is more problematic +/// +/// One bad practice is to use RefCell to make the compiler "happy" +/// using `.borrow_mut()` +/// +/// Even though our code compiles, we've introduced other problems. +/// Rather than using compile-time guarantees, we are now relying on runtime +/// borrow checking, which can potentially panic. +/// +/// The code is also more difficult to reason about and work with, because +/// we are now effectively hiding mutation behind shared references. +impl HistoryManager for Calculator { + fn add_to_history(&self, expression: String, result: f64) { + self.history + .borrow_mut() + .push(CalculationResult { expression, result }); + } + + fn clear_history(&self) { + self.history.borrow_mut().clear(); + } +} diff --git a/design_patterns_in_rust/bad_calculator_2/src/ownership_better_example.rs b/design_patterns_in_rust/bad_calculator_2/src/ownership_better_example.rs @@ -0,0 +1,56 @@ +//! Better calculator: work within ownership rules +//! +//! The code below now avoids runtime checking overhead as in Rc and RefCell, +//! and it's easier to reason about. + +struct CalculationResult { + expression: String, + result: f64, +} + +struct Calculator { + history: Vec<CalculationResult>, + current_expression: Option<String>, +} + +/// If we need to share access to history, we can create a dedicated type +/// +/// A clean view-only type that introduces lifetimes, which are Rust's way of +/// tracking how long references remain valid. +struct HistoryView<'a> { + entries: &'a [CalculationResult], +} + +/// It is clear when mutation can occur because of the method signatures +impl Calculator { + fn new() -> Self { + Self { + history: Vec::new(), + current_expression: None, + } + } + + fn create_history_view(&self) -> HistoryView<'_> { + HistoryView { + entries: &self.history, + } + } + + fn add_to_history(&mut self, expression: String, result: f64) { + self.history.push(CalculationResult { expression, result }); + } + + fn clear_history(&mut self) { + self.history.clear(); + } + + fn calculate_expression(&self, expression: &str) -> Result<f64, String> { + todo!() + } + + fn evaluate(&mut self, expression: String) -> Result<f64, String> { + let result = self.calculate_expression(&expression)?; + self.add_to_history(expression, result); + Ok(result) + } +} diff --git a/design_patterns_in_rust/bad_calculator_2/src/smart_pointers_bad_example.rs b/design_patterns_in_rust/bad_calculator_2/src/smart_pointers_bad_example.rs @@ -0,0 +1,78 @@ +//! Bad practice: over-engieer with smart pointers and wrap everything in +//! Rc<RefCell<_>>, which resulting in the code become cluttered with +//! borrow() and borrow_mut() calls. + +use std::{ + cell::{Ref, RefCell}, + collections::HashMap, + rc::Rc, +}; + +struct Variable { + name: String, + value: f64, +} + +enum Token { + Number(f64), + Variable(Variable), + Operator(char), +} + +struct Expression { + tokens: Rc<RefCell<Vec<Token>>>, + result: Rc<RefCell<Option<f64>>>, +} + +struct Calculator { + current_expression: Rc<RefCell<Option<Expression>>>, + variables: Rc<RefCell<HashMap<String, f64>>>, +} + +/// This code has several serious issues. +/// 1. it is verbose and difficult to reason about +/// 2. we have to manually manage lifetimes +/// 3. there is an inherent cost to reference counting +impl Calculator { + fn new() -> Self { + Self { + current_expression: Rc::new(RefCell::new(None)), + variables: Rc::new(RefCell::new(HashMap::new())), + } + } + + fn tokenize(&self, expr: &str) -> Vec<Token> { + todo!() + } + + fn set_expression(&self, expr: &str) { + let tokens = self.tokenize(expr); + + *self.current_expression.borrow_mut() = Some(Expression { + tokens: Rc::new(RefCell::new(tokens)), + result: Rc::new(RefCell::new(None)), + }) + } + + fn process_tokens( + &self, + tokens: &mut Vec<Token>, + vars: &HashMap<String, f64>, + ) -> Result<f64, String> { + todo!() + } + + fn evaluate(&self) -> Result<f64, String> { + let expr = self.current_expression.borrow(); + let expr = expr.as_ref().ok_or("No expression set")?; + + let mut tokens = expr.tokens.borrow_mut(); + let vars = self.variables.borrow(); + + // Process tokens... + let result = self.process_tokens(&mut tokens, &vars)?; + + *expr.result.borrow_mut() = Some(result); + Ok(result) + } +} diff --git a/design_patterns_in_rust/bad_calculator_2/src/smart_pointers_better_example.rs b/design_patterns_in_rust/bad_calculator_2/src/smart_pointers_better_example.rs @@ -0,0 +1,119 @@ +//! Better design without misusing smart points: +//! Data flow in one direction +//! Each step produces output the next step consumes +//! Calculator owns its variables and history directly, so no shared ownership +//! is needed + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +#[derive(Clone)] +enum Token { + Number(f64), + Variable(String), + Operator(char), +} + +struct ParsedExpression { + tokens: Vec<Token>, +} + +pub struct Calculation { + pub expression: String, + tokens: Vec<Token>, + pub result: f64, +} + +pub struct Calculator { + variables: HashMap<String, f64>, + history: Vec<Calculation>, +} + +/// In each methods, we ask for the kind of reference we need, mutable or immutable, +/// and then have clean compiler verified access to the data +impl Calculator { + pub fn new() -> Self { + Self { + variables: HashMap::new(), + history: Vec::new(), + } + } + + fn tokenize(&self, expr: &str) -> Vec<Token> { + todo!() + } + + fn parse(&self, expr: &str) -> Result<ParsedExpression, String> { + let tokens = self.tokenize(expr); + Ok(ParsedExpression { tokens }) + } + + fn evaluate_tokens(&self, tokens: Vec<Token>) -> Result<f64, String> { + todo!() + } + + fn evaluate_parsed(&mut self, expr: String, parsed: ParsedExpression) -> Result<f64, String> { + let result = self.evaluate_tokens(parsed.tokens.clone())?; + + self.history.push(Calculation { + expression: expr, + tokens: parsed.tokens, + result, + }); + + Ok(result) + } + + // With unambiguous access patterns, we can create convenient methods that + // follows this pattern + pub fn evaluate(&mut self, expr: String) -> Result<f64, String> { + let parsed = self.parse(&expr)?; + self.evaluate_parsed(expr, parsed) + } + + pub fn history(&self) -> &[Calculation] { + &self.history + } + + fn last_result(&self) -> Option<f64> { + self.history.last().map(|calc| calc.result) + } + + pub fn set_variable(&mut self, name: String, value: f64) { + self.variables.insert(name, value); + } + + fn get_variables(&self, name: &str) -> Option<f64> { + self.variables.get(name).copied() + } +} + +/// When we really need thread-safe access, we add it through a dedicated wrapper +pub struct ThreadSafeCalculator { + inner: Arc<Mutex<Calculator>>, +} + +impl ThreadSafeCalculator { + pub fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(Calculator::new())), + } + } + + pub fn evaluate(&self, expr: String) -> Result<f64, String> { + let mut calc = self.inner.lock().map_err(|_| "Lock poinsoned")?; + calc.evaluate(expr) + } + + // Other methods following the same pattern... +} + +impl Clone for ThreadSafeCalculator { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} diff --git a/design_patterns_in_rust/bad_calculator_3/Cargo.lock b/design_patterns_in_rust/bad_calculator_3/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bad_calculator_3" +version = "0.1.0" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" diff --git a/design_patterns_in_rust/bad_calculator_3/Cargo.toml b/design_patterns_in_rust/bad_calculator_3/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "bad_calculator_3" +version = "0.1.0" +edition = "2024" + +[dependencies] +lazy_static = "1.5.0" diff --git a/design_patterns_in_rust/bad_calculator_3/src/bad_example.rs b/design_patterns_in_rust/bad_calculator_3/src/bad_example.rs @@ -0,0 +1,78 @@ +//! This is a bad example demonstrating how circumventing +//! the borrow checker is a bad practice in rust + +use std::cell::RefCell; + +#[derive(Debug)] +enum Token { + Number(f64), + ResultReference(usize), + Operr(char), +} + +/// The struct stores both the current value and a history +/// of all previous results in a vec +struct Calculator { + current_value: f64, + memory: RefCell<Vec<f64>>, +} + +impl Calculator { + fn new() -> Self { + Self { + current_value: 0.0, + memory: RefCell::new(Vec::new()), + } + } + + /// We either retrive a previous result or compute a + /// new one, then update the calculator's state + fn evaluate(&mut self, expression: &str) -> Result<f64, String> { + if expression.starts_with("result") { + if let Some(index) = expression.strip_prefix("result") { + if let Ok(offset) = index.trim().parse::<usize>() { + return self.get_previous_result(offset); + } + } + } + + let result = self.parse_and_evaluate(expression)?; + + self.memory.borrow_mut().push(result); + self.current_value = result; + + Ok(result) + } + + fn get_previous_result(&self, index: usize) -> Result<f64, String> { + if index == 0 { + Ok(self.current_value) + } else { + let memory = self.memory.borrow(); + let pos = memory + .len() + .checked_sub(index) + .ok_or("Invalid result index")?; + memory + .get(pos) + .copied() + .ok_or_else(|| "Invalid result index".to_string()) + } + } + + fn parse_and_evaluate(&self, expression: &str) -> Result<f64, String> { + let tokens = self.tokenize(expression)?; + + for token in &tokens { + if let Token::ResultReference(index) = token { + let prev = self.get_previous_result(*index)?; + } + } + + todo!("Actual evaluation TBD") + } + + fn tokenize(&self, expression: &str) -> Result<Vec<Token>, String> { + todo!() + } +} diff --git a/design_patterns_in_rust/bad_calculator_3/src/bad_example_statics.rs b/design_patterns_in_rust/bad_calculator_3/src/bad_example_statics.rs @@ -0,0 +1,122 @@ +use std::{collections::HashMap, sync::Mutex}; + +use lazy_static::lazy_static; + +lazy_static! { + static ref MEMORY: Mutex<Vec<f64>> = Mutex::new(Vec::new()); + static ref VARIABLES: Mutex<HashMap<String, f64>> = Mutex::new(HashMap::new()); +} + +#[derive(Debug)] +enum Token { + Number(f64), + Variable(String), + Operator(char), +} + +struct Calculator; + +impl Calculator { + fn new() -> Self { + Self + } + + fn store_result(&self, result: f64) { + let mut memory = MEMORY.lock().unwrap(); + memory.push(result); + } + + fn get_previous_result(&self, index: usize) -> Option<f64> { + let memory = MEMORY.lock().unwrap(); + if index == 0 { + memory.last().copied() + } else { + let pos = memory.len().checked_sub(index)?; + memory.get(pos).copied() + } + } + + fn tokenize(&self, expression: &str) -> Result<Vec<Token>, String> { + todo!() + } + + fn evaluate(&self, expression: &str) -> Result<f64, String> { + if let Some((name, value_expr)) = expression.split_once('=') { + let value = self.evaluate(value_expr.trim())?; + let mut vars = VARIABLES.lock().unwrap(); + vars.insert(name.trim().to_string(), value); + return Ok(value); + } + + let result = self.parse_and_evaluate(expression)?; + self.store_result(result); + Ok(result) + } + + fn evaluate_tokens(&self, tokens: Vec<Token>) -> Result<f64, String> { + todo!() + } + + fn parse_and_evaluate(&self, expression: &str) -> Result<f64, String> { + let tokens = self.tokenize(expression)?; + + let mut resolved_tokens = Vec::new(); + + for token in tokens { + match token { + Token::Variable(name) => { + let vars = VARIABLES.lock().unwrap(); + let value = vars + .get(&name) + .ok_or_else(|| format!("Undefined variable: {}", name))?; + resolved_tokens.push(Token::Number(*value)); + } + token => resolved_tokens.push(token), + } + } + + self.evaluate_tokens(resolved_tokens) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn clear_memory() { + let mut memory = MEMORY.lock().unwrap(); + memory.clear(); + } + + fn clear_variables() { + let mut variables = VARIABLES.lock().unwrap(); + variables.clear(); + } + + #[test] + fn test_calculator_results() -> Result<(), String> { + clear_memory(); + clear_variables(); + + let calc = Calculator::new(); + calc.evaluate("x = 5")?; + calc.evaluate("y = x + 3")?; + + assert_eq!(calc.evaluate("y")?, 8.0); + Ok(()) + } + + #[test] + fn test_interdependent_variables() -> Result<(), String> { + clear_memory(); + clear_variables(); + + let calc = Calculator::new(); + + calc.evaluate("a = 1")?; + calc.evaluate("b = a + 1")?; + calc.evaluate("a = b + 1")?; + assert_eq!(calc.evaluate("a")?, 3.0); + Ok(()) + } +} diff --git a/design_patterns_in_rust/bad_calculator_3/src/bad_example_unsafe.rs b/design_patterns_in_rust/bad_calculator_3/src/bad_example_unsafe.rs @@ -0,0 +1,108 @@ +/// It stores the expression string and the computed result +struct CalculatorState { + expression: String, + result: f64, +} + +/// It stores states in a Vec and maintains a raw pointer to the +/// current state +pub struct UnsafeHistory { + states: Vec<CalculatorState>, + current: Option<*const CalculatorState>, +} + +impl UnsafeHistory { + fn new() -> Self { + Self { + states: Vec::with_capacity(10), + current: None, + } + } + + fn push(&mut self, state: CalculatorState) { + self.states.push(state); + self.current = Some(self.states.last().unwrap() as *const CalculatorState); + } + + /// This method dereferences our raw pointer inside an unsafe + /// block to retrieve the result + /// This is like telling the compiler: "Trust me, this pointer + /// is valid"... + pub fn current_result(&self) -> Option<f64> { + self.current.map(|ptr| unsafe { (*ptr).result }) + } + + /// This method finds our current position in the Vec, then moves + /// the current pointer back one slot and returns that result + fn undo(&mut self) -> Option<f64> { + let ptr = self.current?; + + // Find current position and move back one + let pos = self + .states + .iter() + .position(|state| std::ptr::eq(state, ptr))?; + + if pos > 0 { + self.current = Some(&self.states[pos - 1] as *const CalculatorState); + self.current_result() + } else { + None // Already at the beginning + } + } + + /// Move forward in history, then dereference the raw pointer + /// to get its state + fn redo(&mut self) -> Option<f64> { + let ptr = self.current?; + let pos = self.states.iter().position(|s| std::ptr::eq(s, ptr))?; + + if pos + 1 < self.states.len() { + self.current = Some(&self.states[pos + 1] as *const CalculatorState); + self.current_result() + } else { + None // Already at the end + } + } +} + +pub struct Calculator { + pub history: UnsafeHistory, +} + +impl Calculator { + pub fn new() -> Self { + Self { + history: UnsafeHistory::new(), + } + } + + pub fn evaluate(&mut self, expression: &str) -> Result<f64, String> { + if expression == "undo" { + return self + .history + .undo() + .ok_or_else(|| "Nothing to undo".to_string()); + } + + if expression == "redo" { + return self + .history + .redo() + .ok_or_else(|| "Nothing to redo".to_string()); + } + + let result = self.parse_and_evaluate(expression)?; + + self.history.push(CalculatorState { + expression: expression.to_string(), + result, + }); + + Ok(result) + } + + fn parse_and_evaluate(&self, expression: &str) -> Result<f64, String> { + todo!("Evaluation TBD") + } +} diff --git a/design_patterns_in_rust/bad_calculator_3/src/better_example.rs b/design_patterns_in_rust/bad_calculator_3/src/better_example.rs @@ -0,0 +1,70 @@ +#[derive(Debug)] +enum Token { + Number(f64), + ResultReference(usize), + Operator(char), +} + +/// The struct stores both the current value and a history +/// of all previous results in a vec +struct Calculator { + current_value: f64, + memory: Vec<f64>, +} + +impl Calculator { + fn tokenize(&self, expression: &str) -> Result<Vec<Token>, String> { + let mut tokens = Vec::new(); + + for part in expression.split_whitespace() { + let token = if let Some(index) = part.strip_prefix("result") { + if let Ok(offset) = index.trim().parse() { + Token::ResultReference(offset) + } else { + return Err("Invalid result reference".to_string()); + } + } else if let Ok(num) = part.parse() { + Token::Number(num) + } else if part.len() == 1 && "+-*/".contains(part) { + Token::Operator(part.chars().next().unwrap()) + } else { + return Err(format!("Invalid token: {}", part)); + }; + + tokens.push(token); + } + + Ok(tokens) + } + + fn evaluate_tokens(&self, tokens: Vec<Token>) -> Result<f64, String> { + todo!("The evaluator is TBD") + } + + fn evaluate(&mut self, expression: &str) -> Result<f64, String> { + let tokens = self.tokenize(expression)?; + + let mut resolved_tokens = Vec::new(); + + for token in tokens { + match token { + Token::ResultReference(index) => { + let value = self.get_previous_result(index)?; + resolved_tokens.push(Token::Number(value)); + } + token => resolved_tokens.push(token), + } + } + + let result = self.evaluate_tokens(resolved_tokens)?; + + self.memory.push(result); + self.current_value = result; + + Ok(result) + } + + fn get_previous_result(&self, index: usize) -> Result<f64, String> { + todo!() + } +} diff --git a/design_patterns_in_rust/bad_calculator_3/src/better_example_no_statics.rs b/design_patterns_in_rust/bad_calculator_3/src/better_example_no_statics.rs @@ -0,0 +1,89 @@ +use std::collections::HashMap; + +#[derive(Debug)] +enum Token { + Number(f64), + Variable(String), + Operator(char), +} + +struct Calculator { + memory: Vec<f64>, + variables: HashMap<String, f64>, +} + +impl Calculator { + fn new() -> Self { + Self { + memory: Vec::new(), + variables: HashMap::new(), + } + } + + fn tokenize(&self, expression: &str) -> Result<Vec<Token>, String> { + todo!() + } + + fn evaluate(&mut self, expression: &str) -> Result<f64, String> { + if let Some((name, value_expr)) = expression.split_once('=') { + let value = self.evaluate(value_expr.trim())?; + self.variables.insert(name.trim().to_string(), value); + return Ok(value); + } + + let tokens = self.tokenize(expression)?; + let resolved_tokens = self.resolve_variables(tokens)?; + let result = self.evaluate_tokens(resolved_tokens)?; + + self.memory.push(result); + Ok(result) + } + + fn evaluate_tokens(&self, tokens: Vec<Token>) -> Result<f64, String> { + todo!() + } + + fn resolve_variables(&self, tokens: Vec<Token>) -> Result<Vec<Token>, String> { + tokens + .into_iter() + .map(|token| match token { + Token::Variable(name) => { + let value = self + .variables + .get(&name) + .ok_or_else(|| format!("Undefined variable: {}", name))?; + Ok(Token::Number(*value)) + } + + token => Ok(token), + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calculator() -> Result<(), String> { + let mut calc = Calculator::new(); + + calc.evaluate("x = 5")?; + calc.evaluate("y = x + 3")?; + + assert_eq!(calc.evaluate("y")?, 8.0); + Ok(()) + } + + #[test] + fn test_interlocking_variables() -> Result<(), String> { + let mut calc = Calculator::new(); + + calc.evaluate("a = 1")?; + calc.evaluate("b = a + 1")?; + calc.evaluate("a = b + 1")?; + assert_eq!(calc.evaluate("a")?, 3.0); + Ok(()) + } +} diff --git a/design_patterns_in_rust/bad_calculator_3/src/better_example_no_unsafe.rs b/design_patterns_in_rust/bad_calculator_3/src/better_example_no_unsafe.rs @@ -0,0 +1,94 @@ +/// It stores the expression string and the computed result +struct CalculatorState { + expression: String, + result: f64, +} + +/// It stores states in a Vec and maintains a position index to the +/// current state +pub struct History { + states: Vec<CalculatorState>, + position: usize, +} + +impl History { + fn new() -> Self { + Self { + states: Vec::with_capacity(10), + position: 0, + } + } + + fn push(&mut self, state: CalculatorState) { + // When pushing after an undo, discard the "future" states + self.states.truncate(self.position); + self.states.push(state); + self.position = self.states.len(); + } + + pub fn current_result(&self) -> Option<f64> { + if self.position > 0 { + self.states.get(self.position - 1).map(|state| state.result) + } else { + None + } + } + + fn undo(&mut self) -> Option<f64> { + if self.position > 0 { + self.states.get(self.position - 1).map(|state| state.result) + } else { + None + } + } + + fn redo(&mut self) -> Option<f64> { + if self.position < self.states.len() { + self.position += 1; + self.current_result() + } else { + None + } + } +} + +pub struct Calculator { + pub history: UnsafeHistory, +} + +impl Calculator { + pub fn new() -> Self { + Self { + history: UnsafeHistory::new(), + } + } + + pub fn evaluate(&mut self, expression: &str) -> Result<f64, String> { + if expression == "undo" { + return self + .history + .undo() + .ok_or_else(|| "Nothing to undo".to_string()); + } + + if expression == "redo" { + return self + .history + .redo() + .ok_or_else(|| "Nothing to redo".to_string()); + } + + let result = self.parse_and_evaluate(expression)?; + + self.history.push(CalculatorState { + expression: expression.to_string(), + result, + }); + + Ok(result) + } + + fn parse_and_evaluate(&self, expression: &str) -> Result<f64, String> { + todo!("Evaluation TBD") + } +} diff --git a/design_patterns_in_rust/bad_calculator_3/src/lib.rs b/design_patterns_in_rust/bad_calculator_3/src/lib.rs @@ -0,0 +1,6 @@ +pub mod bad_example; +pub mod bad_example_statics; +pub mod bad_example_unsafe; +pub mod better_example; +pub mod better_example_no_statics; +pub mod better_example_no_unsafe; diff --git a/design_patterns_in_rust/bad_calculator_3/src/main.rs b/design_patterns_in_rust/bad_calculator_3/src/main.rs @@ -0,0 +1,17 @@ +use bad_calculator_3::bad_example_unsafe::Calculator; + +fn main() -> Result<(), String> { + let mut calc = Calculator::new(); + + // Add calculations until we exceed capacity + for i in 0..15 { + calc.evaluate(&format!("{} + {}", i, i))?; + } + + // Try to undo + println!("Current: {:?}", calc.history.current_result()); + println!("Undo: {:?}", calc.evaluate("undo")?); + println!("Undo again: {:?}", calc.evaluate("undo")?); + + Ok(()) +} diff --git a/design_patterns_in_rust/good_calculator/Cargo.lock b/design_patterns_in_rust/good_calculator/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "good_calculator" +version = "0.1.0" diff --git a/design_patterns_in_rust/good_calculator/Cargo.toml b/design_patterns_in_rust/good_calculator/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "good_calculator" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/design_patterns_in_rust/good_calculator/src/builder.rs b/design_patterns_in_rust/good_calculator/src/builder.rs @@ -0,0 +1,189 @@ +use crate::{TokenFactory, token::Token}; + +/// This struct represents our final, immutable expression +#[derive(Clone)] +pub struct Expression<F: TokenFactory> { + tokens: Vec<Token<F::Number, F::Operator>>, + factory: F, +} + +impl<F: TokenFactory> Expression<F> { + /// Defining builder on Expression rather than calling ExpressionBuilder::new + /// directly follows Rust convention: the type you're building provides the + /// entry point to its builder, this makes the API discoverable + pub fn builder(factory: F) -> ExpressionBuilder<F> { + ExpressionBuilder::new(factory) + } + + pub fn evaluate(&self) -> Result<F::Number, String> { + todo!() + } + + /// Prototype Pattern + pub fn quadratic_template(factory: F) -> Result<ExpressionBuilder<F>, String> { + Expression::builder(factory) + .number("1")? + .operator("*")? + .variable("x") + .operator("^")? + .number("2")? + .operator("+")? + .number("0")? + .operator("*")? + .variable("x") + .operator("+")? + .number("0") + } + + /// Another Prototype Pattern + pub fn set_coefficient(&self, a: i64, b: f64) -> Result<(), String> { + todo!() + } +} + +#[derive(Clone)] +pub struct SubExpression<F: TokenFactory> { + pub tokens: Vec<Token<F::Number, F::Operator>>, + factory: F, +} + +impl<F: TokenFactory> SubExpression<F> { + pub fn new(tokens: Vec<Token<F::Number, F::Operator>>, factory: F) -> Self { + Self { tokens, factory } + } + + // Insert this subexpression into a larger expression + pub fn insert_into(self, builder: ExpressionBuilder<F>) -> ExpressionBuilder<F> { + builder.extend(self.tokens) + } +} + +/// This struct manages the construction process +pub struct ExpressionBuilder<F: TokenFactory> { + tokens: Vec<Token<F::Number, F::Operator>>, + factory: F, + paren_count: i32, +} + +/// Implement the builder construction methods +/// Each method takes ownership of self and then returns it after modification +/// This enables method chaining while preventing accidental reuse of partially +/// built expressions +impl<F: TokenFactory> ExpressionBuilder<F> { + pub fn new(factory: F) -> Self { + Self { + tokens: Vec::new(), + factory, + paren_count: 0, + } + } + + // This is to support inserting subexpression into a larger expression + pub fn extend(mut self, tokens: Vec<Token<F::Number, F::Operator>>) -> ExpressionBuilder<F> { + todo!() + } + + // Add a number to the expression + pub fn number(mut self, value: &str) -> Result<Self, String> { + let num = self.factory.create_number(value)?; + self.tokens.push(Token::Number(num)); + Ok(self) + } + + // Add an operator + pub fn operator(mut self, op: &str) -> Result<Self, String> { + let op = self.factory.create_operator(op)?; + self.tokens.push(Token::Operator(op)); + Ok(self) + } + + // Add a variable + pub fn variable(mut self, name: &str) -> Self { + self.tokens.push(Token::Variable(name.to_string())); + self + } + + // Open a parenthesis group + pub fn open_paren(mut self) -> Self { + self.tokens.push(Token::OpenParen); + self.paren_count += 1; + self + } + + // Close a parenthesis group + // The guard error prevents invalid expression from being built + pub fn close_paren(mut self) -> Result<Self, String> { + if self.paren_count <= 0 { + return Err("Unmatched closing parenthesis".to_string()); + } + self.tokens.push(Token::CloseParen); + self.paren_count -= 1; + Ok(self) + } + + /// Specialized Builder Pattern + /// Function-application Pattern + pub fn function(mut self, func: &str, arg: &str) -> Result<Self, String> { + let func_op = self.factory.create_operator(func)?; + let arg_num = self.factory.create_number(arg)?; + + self.tokens.push(Token::Operator(func_op)); + self.tokens.push(Token::Number(arg_num)); + + Ok(self) + } + + /// Specialized Builder Pattern + /// Constructs a complete binary expression in one call + pub fn binary_op(mut self, left: &str, op: &str, right: &str) -> Result<Self, String> { + let left_num = self.factory.create_number(left)?; + let op_token = self.factory.create_operator(op)?; + let right_num = self.factory.create_number(right)?; + + self.tokens.extend([ + Token::Number(left_num), + Token::Operator(op_token), + Token::Number(right_num), + ]); + + Ok(self) + } + + // Build the final expression + // This method consumes the build, by taking self by value, ensuring no + // further modifications can be made after building + pub fn build(self) -> Result<Expression<F>, String> { + if self.paren_count != 0 { + return Err("Unmatched parenthesis".to_string()); + } + + if self.tokens.is_empty() { + return Err("Empty expression".to_string()); + } + + // Validate expression structure + self.validate_expression()?; + + Ok(Expression { + tokens: self.tokens, + factory: self.factory, + }) + } + + fn validate_expression(&self) -> Result<(), String> { + use Token::*; + + // No consecutive operators + for window in self.tokens.windows(2) { + match (&window[0], &window[1]) { + (Operator(_), Operator(_)) => { + return Err("Consecutive operators".to_string()); + } + _ => continue, + } + } + + // More validation rules... + Ok(()) + } +} diff --git a/design_patterns_in_rust/good_calculator/src/calculator.rs b/design_patterns_in_rust/good_calculator/src/calculator.rs @@ -0,0 +1,138 @@ +use std::sync::Arc; + +use crate::{ + TokenFactory, + factory::StandardFactory, + number::{AngleMode, NumberFormat}, + token::Token, +}; + +// /// Singleton Pattern? +// /// This is rarely the best solution in Rust +// pub struct CalculatorSettings { +// precision: u32, +// angle_mode: AngleMode, +// notation: NumberFormat, +// } +// impl CalculatorSettings { +// pub fn instance() -> &'static mut Self { +// static mut INSTANCE: Option<CalculatorSettings> = None; + +// // The traditional singleton pattern requires unsafe, because we're +// // creating mutable static variable; it's also not thread-safe +// unsafe { +// INSTANCE.get_or_insert_with(|| Self { +// precision: 10, +// angle_mode: AngleMode::Radians, +// notation: NumberFormat::Decimal, +// }) +// } +// } +// } + +/// Instead of Singleton Pattern, we will build something more robust that +/// actually does what we need it to do: +/// - this struct replaces the mutable global state +/// - the Default implementation provide sensible defaults +/// - because this struct is Clone, each calculator can have its own copy, +/// eliminating shared mutable state +#[derive(Clone, Debug, Default)] +pub struct CalculatorConfig { + precision: u32, + angle_mode: AngleMode, + notation: NumberFormat, +} + +// Use Default trait instead +// impl Default for CalculatorConfig { +// fn default() -> Self { +// Self { +// precision: 10, +// angle_mode: AngleMode::Radians, +// notation: NumberFormat::Decimal, +// } +// } +// } + +impl CalculatorConfig { + pub fn scientific() -> Self { + Self { + precision: 15, + angle_mode: AngleMode::Radians, + notation: NumberFormat::Scientific, + ..Default::default() + } + } + + pub fn engineering() -> Self { + Self { + notation: NumberFormat::Engineering, + ..Default::default() + } + } +} + +pub struct Calculator<F: TokenFactory> { + config: CalculatorConfig, + factory: F, + expression: Vec<Token<F::Number, F::Operator>>, +} + +impl<F: TokenFactory> Calculator<F> { + pub fn new(factory: F) -> Self { + Self { + config: CalculatorConfig::default(), + factory, + expression: Vec::new(), + } + } + + /// It takes in a configuration explicitly through its constructor + /// This dependency injection approach makes the calculator's requirement + /// visible in its API. It has no hidden global state that might change + /// unexpectedly + pub fn with_config(factory: F, config: CalculatorConfig) -> Self { + Self { + config, + factory, + expression: Vec::new(), + } + } + + pub fn parse(&mut self, input: &str) -> Result<(), String> { + for token in input.split_whitespace() { + // Try operator first + if let Ok(op) = self.factory.create_operator(token) { + self.expression.push(Token::Operator(op)); + continue; + } + + // Must be a number then + let num = self.factory.create_number(token)?; + self.expression.push(Token::Number(num)); + } + + Ok(()) + } +} + +pub struct CalculatorPool { + shared_config: Arc<CalculatorConfig>, + calculators: Vec<Calculator<StandardFactory>>, +} + +impl CalculatorPool { + pub fn new(config: CalculatorConfig) -> Self { + Self { + shared_config: Arc::new(config), + calculators: Vec::new(), + } + } + + pub fn new_calculator(&mut self) -> Calculator<StandardFactory> { + // let calc = Calculator::with_config(StandardFactory, (*self.shared_config).clone()); + // self.calculators.push(calc.clone()); + // calc + todo!() + } +} diff --git a/design_patterns_in_rust/good_calculator/src/factory.rs b/design_patterns_in_rust/good_calculator/src/factory.rs @@ -0,0 +1,63 @@ +use crate::{ + TokenFactory, + number::{NumberFormat, ScientificNumber, StandardNumber}, + operator::{Operator, ScientificOperator, StandardOperator}, + token::Function, +}; + +#[derive(Clone)] +pub struct StandardFactory; + +impl TokenFactory for StandardFactory { + type Number = StandardNumber; + type Operator = StandardOperator; + + fn create_number(&self, s: &str) -> Result<Self::Number, String> { + s.parse::<f64>() + .map(StandardNumber) + .map_err(|_| format!("Invalid number: {}", s)) + } + + fn create_operator(&self, s: &str) -> Result<Self::Operator, String> { + match s { + "+" => Ok(StandardOperator(Operator::Add)), + "-" => Ok(StandardOperator(Operator::Subtract)), + "*" => Ok(StandardOperator(Operator::Multiply)), + "/" => Ok(StandardOperator(Operator::Divide)), + _ => Err(format!("Invalid operator: {}", s)), + } + } +} + +pub struct ScientificFactory; + +impl TokenFactory for ScientificFactory { + type Number = ScientificNumber; + type Operator = ScientificOperator; + + fn create_number(&self, s: &str) -> Result<Self::Number, String> { + // Handle both scientific and standard notation + if s.contains('e') || s.contains('E') { + s.parse::<f64>().map(|value| ScientificNumber { + value, + format: NumberFormat::Scientific, + }) + } else { + s.parse::<f64>().map(|value| ScientificNumber { + value, + format: NumberFormat::Decimal, + }) + } + .map_err(|_| format!("Invalid number: {}", s)) + } + + fn create_operator(&self, s: &str) -> Result<Self::Operator, String> { + // Scientific mode support more operators + match s { + "sin" => Ok(ScientificOperator::Function(Function::Sin)), + "cos" => Ok(ScientificOperator::Function(Function::Cos)), + // ...other scientifc operators + _ => Err(format!("Invalid operator: {}", s)), + } + } +} diff --git a/design_patterns_in_rust/good_calculator/src/lib.rs b/design_patterns_in_rust/good_calculator/src/lib.rs @@ -0,0 +1,28 @@ +pub mod builder; +pub mod calculator; +pub mod factory; +pub mod number; +pub mod operator; +pub mod token; + +/// This trait defines all number types across different calculator modes +pub trait NumberToken { + fn value(&self) -> f64; + fn format(&self) -> String; +} + +pub trait OperatorToken { + fn precedence(&self) -> u8; + fn evaluate(&self, args: &[f64]) -> Result<f64, String>; +} + +/// TokenFactory trait that ties everything together. It ensures that tokens +/// created by the factory are always compatible. +pub trait TokenFactory { + /// The associated Number and Operator types are the key to type safety + type Number: NumberToken; + type Operator: OperatorToken; + + fn create_number(&self, s: &str) -> Result<Self::Number, String>; + fn create_operator(&self, s: &str) -> Result<Self::Operator, String>; +} diff --git a/design_patterns_in_rust/good_calculator/src/main.rs b/design_patterns_in_rust/good_calculator/src/main.rs @@ -0,0 +1,92 @@ +use good_calculator::{ + builder::{Expression, SubExpression}, + calculator::{Calculator, CalculatorConfig}, + factory::{ScientificFactory, StandardFactory}, + number::StandardNumber, + operator::{Operator, StandardOperator}, + token::Token, +}; + +fn main() -> Result<(), String> { + // 1. Enum in Vec + // + // In Rust Vec has to be a single type, the Token enum conviniently allows + // this by providing a unified type that can hold any token variant + // let numbers = [ + // Token::number(6.0), + // Token::operator(Operator::Add), + // Token::Number(3.0) + // ] + + /// 2. Abstract Factory + /// + /// With the abstract factory pattern, we can create different kinds of + /// calculators just by specifying which factory we would like to use: + let standard_calc = Calculator::new(StandardFactory); + let scientific_calc = Calculator::new(ScientificFactory); + + /// 3. Builder Pattern + /// + /// This is how the Builder Pattern and Abstract Factory Pattern compose: + /// the builder delegates token creation to whichever factory it was given + // With standard calculator factory + let expr = Expression::builder(StandardFactory) + .number("2")? + .operator("+")? + .open_paren() + .number("3")? + .operator("*")? + .number("4")? + .close_paren()? + .build(); + // With scientific calculator factory + let expr = Expression::builder(ScientificFactory) + .number("1.23e-4")? + .operator("sin")? + .build()?; + + /// 3. Specialized Builder Pattern + /// + /// This encapsulate common expression patterns, so that we can write our + /// expressions even more consicely + let expr = Expression::builder(StandardFactory) + .binary_op("2", "+", "3")? + .build()?; + let expr = Expression::builder(ScientificFactory) + .function("sin", "0.5")? + .build()?; + + /// 4. Prototype Pattern + /// + /// The quadratic_template method creates a prototype with placeholder + /// coefficients. We can clone this template and modify the coefficients. + let expr = Expression::quadratic_template(StandardFactory)?.build()?; + let expr2 = expr.clone(); + expr2.set_coefficient(2, -4.0)?; + /// The sub expression can be cloned and inserted into larger expressions + let squared = SubExpression::new( + vec![ + Token::variable("x".to_string()), + Token::Operator(StandardOperator(Operator::Power)), + Token::Number(StandardNumber(2.0)), + ], + StandardFactory, + ); + /// prototype patterns that use Clone trait naturally: + let expr1 = Expression::builder(StandardFactory) + .extend(squared.clone().tokens) + .operator("+")? + .number("1")? + .build()?; + let expr2 = Expression::builder(StandardFactory) + .number("2")? + .operator("*")? + .extend(squared.tokens) + .build()?; + /// use Default trait + let default_calc = Calculator::new(StandardFactory); + let scientific_calc = + Calculator::with_config(ScientificFactory, CalculatorConfig::scientific()); + + Ok(()) +} diff --git a/design_patterns_in_rust/good_calculator/src/number.rs b/design_patterns_in_rust/good_calculator/src/number.rs @@ -0,0 +1,77 @@ +use crate::NumberToken; + +#[derive(Debug, Clone, PartialEq)] +pub struct Number { + pub value: f64, + pub format: NumberFormat, +} + +impl Number { + pub fn format(&self) -> String { + match self.format { + NumberFormat::Decimal => format!("{}", self.value), + NumberFormat::Scientific => format!("{:e}", self.value), + NumberFormat::Engineering => { + // Engineering notation adjusts exponent to be multiple of 3 + let exp = self.value.abs().log10().floor(); + let adj_exp = (exp - exp % 3.0).floor(); + let coeff = self.value / 10_f64.powf(adj_exp); + format!("{}e{}", coeff, adj_exp) + } + } + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub enum NumberFormat { + #[default] + Decimal, + Scientific, + Engineering, +} + +#[derive(Debug, Clone, Default)] +pub enum AngleMode { + Degrees, + #[default] + Radians, +} + +#[derive(Clone)] +pub struct StandardNumber(pub f64); + +impl NumberToken for StandardNumber { + fn value(&self) -> f64 { + self.0 + } + + fn format(&self) -> String { + format!("{}", self.0) + } +} + +#[derive(Clone)] +pub struct ScientificNumber { + pub value: f64, + pub format: NumberFormat, +} + +/// Different factory families can have different internal representations +impl NumberToken for ScientificNumber { + fn value(&self) -> f64 { + self.value + } + + fn format(&self) -> String { + match self.format { + NumberFormat::Scientific => format!("{:e}", self.value), + NumberFormat::Engineering => { + let exp = self.value.abs().log10().floor(); + let adj_exp = (exp - exp % 3.0).floor(); + let coeff = self.value / 10_f64.powf(adj_exp); + format!("{}e{}", coeff, adj_exp) + } + _ => format!("{}", self.value), + } + } +} diff --git a/design_patterns_in_rust/good_calculator/src/operator.rs b/design_patterns_in_rust/good_calculator/src/operator.rs @@ -0,0 +1,59 @@ +use crate::{OperatorToken, token::Function}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Operator { + Add, + Subtract, + Multiply, + Divide, + Power, + Root, + Factorial, +} + +#[derive(Clone)] +pub struct StandardOperator(pub Operator); + +impl OperatorToken for StandardOperator { + fn precedence(&self) -> u8 { + match self.0 { + Operator::Add | Operator::Subtract => 1, + Operator::Multiply | Operator::Divide => 2, + Operator::Power => 3, + Operator::Root | Operator::Factorial => 4, + } + } + + fn evaluate(&self, args: &[f64]) -> Result<f64, String> { + match self.0 { + Operator::Add => Ok(args[0] + args[1]), + Operator::Subtract => Ok(args[0] - args[1]), + Operator::Multiply => Ok(args[0] * args[1]), + Operator::Divide => { + if args[1] == 0.0 { + Err("Division by zero".to_string()) + } else { + Ok(args[0] / args[1]) + } + } + // ... other operators + _ => Err("Operation not supported in standard mode".to_string()), + } + } +} + +#[derive(Clone)] +pub enum ScientificOperator { + Basic(Operator), + Function(Function), +} + +impl OperatorToken for ScientificOperator { + fn precedence(&self) -> u8 { + todo!() + } + + fn evaluate(&self, args: &[f64]) -> Result<f64, String> { + todo!() + } +} diff --git a/design_patterns_in_rust/good_calculator/src/token.rs b/design_patterns_in_rust/good_calculator/src/token.rs @@ -0,0 +1,51 @@ +use std::borrow::Cow; + +/// Instead of using class hierarchy with factory methods for each type, in Rust +/// we can take a more natural approach, using enums (which provide sum type +/// functionality) +/// +/// The Token enum is now generic over Number and Operator types to support +/// different factory implementations. +#[derive(Debug, Clone, PartialEq)] +pub enum Token<N, O> { + Number(N), + Operator(O), + Function(Function), + Variable(String), + OpenParen, + CloseParen, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Function { + Sqrt, + Sin, + Cos, + Tan, +} + +/// Constructors for the different Token enum types +impl<N, O> Token<N, O> { + pub fn function(func: Function) -> Self { + Self::Function(func) + } + + pub fn variable(name: impl Into<String>) -> Self { + Self::Variable(name.into()) + } +} + +/// Using Cow for Strings and Slices - Prototype Pattern +pub struct VariableToken<'a> { + name: Cow<'a, str>, + value: f64, +} + +impl<'a> VariableToken<'a> { + pub fn new(name: impl Into<Cow<'a, str>>, value: f64) -> Self { + Self { + name: name.into(), + value, + } + } +} diff --git a/design_patterns_in_rust/good_calculator_2/Cargo.lock b/design_patterns_in_rust/good_calculator_2/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "good_calculator" +version = "0.1.0" diff --git a/design_patterns_in_rust/good_calculator_2/Cargo.toml b/design_patterns_in_rust/good_calculator_2/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "good_calculator" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/design_patterns_in_rust/good_calculator_2/src/adapters.rs b/design_patterns_in_rust/good_calculator_2/src/adapters.rs @@ -0,0 +1,114 @@ +//! Adapter Pattern + +use std::{collections::HashMap, f64::consts::PI}; + +use crate::{number::AngleMode, token::VariableToken, utilities::Expression}; + +pub trait ScientificOperations { + fn sin(&self, angle: f64) -> f64; + fn cos(&self, angle: f64) -> f64; + fn tan(&self, angle: f64) -> f64; + fn log(&self, value: f64, base: f64) -> Result<f64, String>; +} + +pub struct StandardScientificOperations { + pub angle_mode: AngleMode, +} + +impl ScientificOperations for StandardScientificOperations { + fn sin(&self, angle: f64) -> f64 { + match self.angle_mode { + AngleMode::Radians => angle.sin(), + AngleMode::Degrees => (angle * PI / 180.0).sin(), + } + } + + fn cos(&self, angle: f64) -> f64 { + match self.angle_mode { + AngleMode::Radians => angle.cos(), + AngleMode::Degrees => (angle * PI / 180.0).cos(), + } + } + + fn tan(&self, angle: f64) -> f64 { + todo!() + } + + fn log(&self, value: f64, base: f64) -> Result<f64, String> { + if value <= 0.0 { + return Err("Cannot take logarithm of non-positive number".to_string()); + } + if base <= 0.0 || base == 1.0 { + return Err("Invalid logarithm base".to_string()); + } + Ok(value.ln() / base.ln()) + } +} + +pub struct ExternalLibraryAdapter { + angle_mode: AngleMode, +} + +impl ExternalLibraryAdapter { + pub fn new(angle_mode: AngleMode) -> Self { + Self { angle_mode } + } + + fn convert_angle(&self, angle: f64) -> f64 { + match self.angle_mode { + AngleMode::Radians => angle, + AngleMode::Degrees => angle * PI / 180.0, + } + } +} + +impl ScientificOperations for ExternalLibraryAdapter { + fn sin(&self, angle: f64) -> f64 { + let converted = self.convert_angle(angle); + converted.sin() + } + + fn cos(&self, angle: f64) -> f64 { + todo!() + } + + fn tan(&self, angle: f64) -> f64 { + todo!() + } + + fn log(&self, value: f64, base: f64) -> Result<f64, String> { + todo!() + } +} + +/// This struct uses a closure (Box<dyn Fn(f64) -> f64) to capture a specific +/// scientific operation. +pub struct ScientificFunctionExpression { + operation: Box<dyn Fn(f64) -> f64>, + arg_expression: Box<dyn Expression>, + description: String, +} + +impl Expression for ScientificFunctionExpression { + fn evaluate(&self, variables: &HashMap<String, f64>) -> Result<f64, String> { + let arg_value = self.arg_expression.evaluate(variables)?; + Ok((self.operation)(arg_value)) + } + + fn to_string(&self) -> String { + format!("{}({})", self.description, self.arg_expression.to_string()) + } +} + +/// The move keyword transfers ownership of the `sci_ops` trait object into +/// the closure. +impl ScientificFunctionExpression { + pub fn new_sin(sci_ops: Box<dyn ScientificOperations>, arg: Box<dyn Expression>) -> Self { + let operation = Box::new(move |angle: f64| sci_ops.sin(angle)); + Self { + operation, + arg_expression: arg, + description: "sin".to_string(), + } + } +} diff --git a/design_patterns_in_rust/good_calculator_2/src/bridge_pattern.rs b/design_patterns_in_rust/good_calculator_2/src/bridge_pattern.rs @@ -0,0 +1,88 @@ +//! The Bridge Pattern: +//! The abstraction defines what the display can do. The implementation defines +//! how formatting works. + +use crate::Expression; + +/// The Display trait speaks in calculator domain concepts: results, errors, +/// expressions. +pub trait Display { + fn show_result(&self, result: f64); + fn show_error(&self, error: &str); + fn show_expression(&self, expression: &dyn Expression); +} + +/// The DisplayImplementation trait spesaks in formatting primitives: text +/// and formatted values. +pub trait DisplayImplementation { + fn display_text(&self, text: &str); + fn display_formatted(&self, value: f64, format: &str); +} + +/// The Bridge connects them through composition: +pub struct CalculatorDisplay { + implementation: Box<dyn DisplayImplementation>, +} + +impl Display for CalculatorDisplay { + fn show_result(&self, result: f64) { + self.implementation + .display_formatted(result, "Result: {:.10g}"); + } + + fn show_error(&self, error: &str) { + self.implementation + .display_text(&format!("Error: {}", error)); + } + + fn show_expression(&self, expression: &dyn Expression) { + self.implementation + .display_text(&format!("Expression: {}", expression.to_string())); + } +} + +/// Concrete output mechanisms: +/// +/// The console implementation writes plain text to standard output: +pub struct ConsoleDisplay; + +impl DisplayImplementation for ConsoleDisplay { + fn display_text(&self, text: &str) { + println!("{}", text); + } + + fn display_formatted(&self, value: f64, format: &str) { + println!("{}", format.replace("{:.10g}", &format!("{:.10}", value))) + } +} + +/// The HTML implementation wraps output in HTML elements: +pub struct HtmlDisplay; + +impl DisplayImplementation for HtmlDisplay { + fn display_text(&self, text: &str) { + println!( + "<div>{}</div>", + text.replace("<", "&lt;").replace(">", "&gt;") + ); + } + + fn display_formatted(&self, value: f64, format: &str) { + let formatted = format.replace("{:.10g}", &format!("{:.10}", value)); + println!("<div class=\"result\">{}</div>", formatted); + } +} + +/// The JSON implementation produces machine-readable output, making it easy +/// to use the output in other programs +pub struct JsonDisplay; + +impl DisplayImplementation for JsonDisplay { + fn display_text(&self, text: &str) { + println!("{{\"text\": \"{}\"}}", text.replace("\"", "\\\"")); + } + + fn display_formatted(&self, value: f64, format: &str) { + println!("{{\"result\": {:.10}}}", value); + } +} diff --git a/design_patterns_in_rust/good_calculator_2/src/builder.rs b/design_patterns_in_rust/good_calculator_2/src/builder.rs @@ -0,0 +1,191 @@ +use crate::{TokenFactory, token::Token}; + +/// This struct represents our final, immutable expression +#[derive(Clone)] +pub struct Expression<F: TokenFactory> { + tokens: Vec<Token<F::Number, F::Operator>>, + factory: F, +} + +impl<F: TokenFactory> Expression<F> { + /// Defining builder on Expression rather than calling ExpressionBuilder::new + /// directly follows Rust convention: the type you're building provides the + /// entry point to its builder, this makes the API discoverable + pub fn builder(factory: F) -> ExpressionBuilder<F> { + ExpressionBuilder::new(factory) + } + + pub fn evaluate(&self) -> Result<F::Number, String> { + todo!() + } + + /// Prototype Pattern + pub fn quadratic_template(factory: F) -> Result<ExpressionBuilder<F>, String> { + Expression::builder(factory) + .number("1")? + .operator("*")? + .variable("x") + .operator("^")? + .number("2")? + .operator("+")? + .number("0")? + .operator("*")? + .variable("x") + .operator("+")? + .number("0") + } + + /// Another Prototype Pattern + pub fn set_coefficient(&self, a: i64, b: f64) -> Result<(), String> { + todo!() + } +} + +#[derive(Clone)] +pub struct SubExpression<F: TokenFactory> { + pub tokens: Vec<Token<F::Number, F::Operator>>, + factory: F, +} + +impl<F: TokenFactory> SubExpression<F> { + pub fn new(tokens: Vec<Token<F::Number, F::Operator>>, factory: F) -> Self { + Self { tokens, factory } + } + + // Insert this subexpression into a larger expression + pub fn insert_into(self, builder: ExpressionBuilder<F>) -> ExpressionBuilder<F> { + builder.extend(self.tokens) + } +} + +/// This struct manages the construction process +pub struct ExpressionBuilder<F: TokenFactory> { + tokens: Vec<Token<F::Number, F::Operator>>, + factory: F, + paren_count: i32, +} + +/// Implement the builder construction methods +/// Each method takes ownership of self and then returns it after modification +/// This enables method chaining while preventing accidental reuse of partially +/// built expressions +impl<F: TokenFactory> ExpressionBuilder<F> { + pub fn new(factory: F) -> Self { + Self { + tokens: Vec::new(), + factory, + paren_count: 0, + } + } + + // This is to support inserting subexpression into a larger expression + pub fn extend(mut self, tokens: Vec<Token<F::Number, F::Operator>>) -> ExpressionBuilder<F> { + todo!() + } + + // Add a number to the expression + pub fn number(mut self, value: &str) -> Result<Self, String> { + let num = self.factory.create_number(value)?; + self.tokens.push(Token::Number(num)); + Ok(self) + } + + // Add an operator + pub fn operator(mut self, op: &str) -> Result<Self, String> { + let op = self.factory.create_operator(op)?; + self.tokens.push(Token::Operator(op)); + Ok(self) + } + + // Add a variable + pub fn variable(mut self, name: &str) -> Self { + self.tokens.push(Token::Variable(name.to_string())); + self + } + + // Open a parenthesis group + pub fn open_paren(mut self) -> Self { + self.tokens.push(Token::OpenParen); + self.paren_count += 1; + self + } + + // Close a parenthesis group + // The guard error prevents invalid expression from being built + pub fn close_paren(mut self) -> Result<Self, String> { + if self.paren_count <= 0 { + return Err("Unmatched closing parenthesis".to_string()); + } + self.tokens.push(Token::CloseParen); + self.paren_count -= 1; + Ok(self) + } + + /// Specialized Builder Pattern + /// Function-application Pattern + pub fn function(mut self, func: &str, arg: &str) -> Result<Self, String> { + let func_op = self.factory.create_operator(func)?; + let arg_num = self.factory.create_number(arg)?; + + self.tokens.push(Token::Operator(func_op)); + self.tokens.push(Token::Number(arg_num)); + + Ok(self) + } + + /// Specialized Builder Pattern + /// Constructs a complete binary expression in one call + pub fn binary_op(mut self, left: &str, op: &str, right: &str) -> Result<Self, String> { + let left_num = self.factory.create_number(left)?; + let op_token = self.factory.create_operator(op)?; + let right_num = self.factory.create_number(right)?; + + self.tokens.extend([ + Token::Number(left_num), + Token::Operator(op_token), + Token::Number(right_num), + ]); + + Ok(self) + } + + // Build the final expression + // This method consumes the build, by taking self by value, ensuring no + // further modifications can be made after building + pub fn build(self) -> Result<Expression<F>, String> { + if self.paren_count != 0 { + return Err("Unmatched parenthesis".to_string()); + } + + if self.tokens.is_empty() { + return Err("Empty expression".to_string()); + } + + // Validate expression structure + self.validate_expression()?; + + Ok(Expression { + tokens: self.tokens, + factory: self.factory, + }) + } + + fn validate_expression(&self) -> Result<(), String> { + use Token::*; + + // No consecutive operators + for window in self.tokens.windows(2) { + match (&window[0], &window[1]) { + (Operator(_), Operator(_)) => { + return Err("Consecutive operators".to_string()); + } + _ => continue, + } + } + + // More validation rules... + Ok(()) + } +} + +pub struct ExpressionParser; diff --git a/design_patterns_in_rust/good_calculator_2/src/calculator.rs b/design_patterns_in_rust/good_calculator_2/src/calculator.rs @@ -0,0 +1,122 @@ +use std::sync::Arc; + +use crate::{ + TokenFactory, + factory::StandardFactory, + number::{AngleMode, NumberFormat}, + token::Token, +}; + +/// Instead of Singleton Pattern, we will build something more robust that +/// actually does what we need it to do: +/// - this struct replaces the mutable global state +/// - the Default implementation provide sensible defaults +/// - because this struct is Clone, each calculator can have its own copy, +/// eliminating shared mutable state +#[derive(Clone, Debug, Default)] +pub struct CalculatorConfig { + precision: u32, + angle_mode: AngleMode, + notation: NumberFormat, +} + +// Use Default trait instead +// impl Default for CalculatorConfig { +// fn default() -> Self { +// Self { +// precision: 10, +// angle_mode: AngleMode::Radians, +// notation: NumberFormat::Decimal, +// } +// } +// } + +impl CalculatorConfig { + pub fn scientific() -> Self { + Self { + precision: 15, + angle_mode: AngleMode::Radians, + notation: NumberFormat::Scientific, + ..Default::default() + } + } + + pub fn engineering() -> Self { + Self { + notation: NumberFormat::Engineering, + ..Default::default() + } + } +} + +pub struct Calculator<F: TokenFactory> { + config: CalculatorConfig, + factory: F, + expression: Vec<Token<F::Number, F::Operator>>, +} + +impl<F: TokenFactory> Calculator<F> { + pub fn new(factory: F) -> Self { + Self { + config: CalculatorConfig::default(), + factory, + expression: Vec::new(), + } + } + + /// It takes in a configuration explicitly through its constructor + /// This dependency injection approach makes the calculator's requirement + /// visible in its API. It has no hidden global state that might change + /// unexpectedly + pub fn with_config(factory: F, config: CalculatorConfig) -> Self { + Self { + config, + factory, + expression: Vec::new(), + } + } + + pub fn parse(&mut self, input: &str) -> Result<(), String> { + for token in input.split_whitespace() { + // Try operator first + if let Ok(op) = self.factory.create_operator(token) { + self.expression.push(Token::Operator(op)); + continue; + } + + // Must be a number then + let num = self.factory.create_number(token)?; + self.expression.push(Token::Number(num)); + } + + Ok(()) + } +} + +pub struct CalculatorPool { + shared_config: Arc<CalculatorConfig>, + calculators: Vec<Calculator<StandardFactory>>, +} + +impl CalculatorPool { + pub fn new(config: CalculatorConfig) -> Self { + Self { + shared_config: Arc::new(config), + calculators: Vec::new(), + } + } + + pub fn new_calculator(&mut self) -> Calculator<StandardFactory> { + // let calc = Calculator::with_config(StandardFactory, (*self.shared_config).clone()); + // self.calculators.push(calc.clone()); + // calc + todo!() + } + + /// Flyweight Pattern: + /// For shared ownerhips of data that needs to live as long as any of its + /// users, Arc (atomic reference counting) provides thread-safe sharing: + pub fn get_config(&self) -> Arc<CalculatorConfig> { + Arc::clone(&self.shared_config) + } +} diff --git a/design_patterns_in_rust/good_calculator_2/src/facade.rs b/design_patterns_in_rust/good_calculator_2/src/facade.rs @@ -0,0 +1,55 @@ +//! The Facade pattern provides a simplified interface to a complex subsystem, +//! hiding the details of multiple interacting components behind a single, +//! easy-to-use API. + +use std::collections::HashMap; + +use crate::{ + adapters::ScientificOperations, builder::ExpressionParser, calculator::CalculatorConfig, +}; + +pub struct CalculatorFacade { + parser: ExpressionParser, + variables: HashMap<String, f64>, + scientific_ops: Box<dyn ScientificOperations>, + history: Vec<String>, + config: CalculatorConfig, +} + +impl CalculatorFacade { + pub fn new(scientific_ops: Box<dyn ScientificOperations>, config: CalculatorConfig) -> Self { + Self { + parser: ExpressionParser, + variables: HashMap::new(), + scientific_ops, + history: Vec::new(), + config, + } + } + + pub fn evaluate(&mut self, expression: &str) -> Result<f64, String> { + self.history.push(expression.to_string()); + + todo!() + } + + pub fn get_variable(&mut self, name: &str, value: f64) { + self.variables.insert(name.to_string(), value); + } + + pub fn calculate_quadric(&self, a: f64, b: f64, c: f64) -> Result<(f64, f64), String> { + let discriminant = b * b - 4.0 * a * c; + if discriminant < 0.0 { + return Err("No real solutions".to_string()); + } + let sqrt_d = discriminant.sqrt(); + let x1 = (-b + sqrt_d) / (2.0 * a); + let x2 = (-b - sqrt_d) / (2.0 * a); + + Ok((x1, x2)) + } + + pub fn calculate_pythagorean(&self, a: f64, b: f64) -> f64 { + (a * a + b * b).sqrt() + } +} diff --git a/design_patterns_in_rust/good_calculator_2/src/factory.rs b/design_patterns_in_rust/good_calculator_2/src/factory.rs @@ -0,0 +1,63 @@ +use crate::{ + TokenFactory, + number::{NumberFormat, ScientificNumber, StandardNumber}, + operator::{Operator, ScientificOperator, StandardOperator}, + token::Function, +}; + +#[derive(Clone)] +pub struct StandardFactory; + +impl TokenFactory for StandardFactory { + type Number = StandardNumber; + type Operator = StandardOperator; + + fn create_number(&self, s: &str) -> Result<Self::Number, String> { + s.parse::<f64>() + .map(StandardNumber) + .map_err(|_| format!("Invalid number: {}", s)) + } + + fn create_operator(&self, s: &str) -> Result<Self::Operator, String> { + match s { + "+" => Ok(StandardOperator(Operator::Add)), + "-" => Ok(StandardOperator(Operator::Subtract)), + "*" => Ok(StandardOperator(Operator::Multiply)), + "/" => Ok(StandardOperator(Operator::Divide)), + _ => Err(format!("Invalid operator: {}", s)), + } + } +} + +pub struct ScientificFactory; + +impl TokenFactory for ScientificFactory { + type Number = ScientificNumber; + type Operator = ScientificOperator; + + fn create_number(&self, s: &str) -> Result<Self::Number, String> { + // Handle both scientific and standard notation + if s.contains('e') || s.contains('E') { + s.parse::<f64>().map(|value| ScientificNumber { + value, + format: NumberFormat::Scientific, + }) + } else { + s.parse::<f64>().map(|value| ScientificNumber { + value, + format: NumberFormat::Decimal, + }) + } + .map_err(|_| format!("Invalid number: {}", s)) + } + + fn create_operator(&self, s: &str) -> Result<Self::Operator, String> { + // Scientific mode support more operators + match s { + "sin" => Ok(ScientificOperator::Function(Function::Sin)), + "cos" => Ok(ScientificOperator::Function(Function::Cos)), + // ...other scientifc operators + _ => Err(format!("Invalid operator: {}", s)), + } + } +} diff --git a/design_patterns_in_rust/good_calculator_2/src/lib.rs b/design_patterns_in_rust/good_calculator_2/src/lib.rs @@ -0,0 +1,44 @@ +mod adapters; +mod bridge_pattern; +mod builder; +mod calculator; +mod facade; +mod factory; +mod number; +mod operator; +mod token; +mod utilities; + +pub use operator::{BinaryOperation, Operator}; +/// Modules and Crates as Facade Pattern +/// +/// By controlling what a module or crate exports through `pub use` re-exports, +/// you create a curated public API that hides internal complexity. +/// +/// This lib.rs file acts as a facade at the crate level. +pub use utilities::{ + CachingExpression, ConsoleLogger, Expression, LoggingExpression, NumberExpression, + TimingExpression, +}; + +/// This trait defines all number types across different calculator modes +pub trait NumberToken { + fn value(&self) -> f64; + fn format(&self) -> String; +} + +pub trait OperatorToken { + fn precedence(&self) -> u8; + fn evaluate(&self, args: &[f64]) -> Result<f64, String>; +} + +/// TokenFactory trait that ties everything together. It ensures that tokens +/// created by the factory are always compatible. +pub trait TokenFactory { + /// The associated Number and Operator types are the key to type safety + type Number: NumberToken; + type Operator: OperatorToken; + + fn create_number(&self, s: &str) -> Result<Self::Number, String>; + fn create_operator(&self, s: &str) -> Result<Self::Operator, String>; +} diff --git a/design_patterns_in_rust/good_calculator_2/src/main.rs b/design_patterns_in_rust/good_calculator_2/src/main.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use good_calculator::{ + BinaryOperation, CachingExpression, ConsoleLogger, Expression, LoggingExpression, + NumberExpression, Operator, TimingExpression, +}; + +fn main() -> Result<(), String> { + /// Structural Patterns + /// + /// 1. Decorator Pattern + let expr = Box::new(NumberExpression::new(42.0)); + let cached = Box::new(CachingExpression::new(expr)); + let timed = Box::new(TimingExpression::new(cached)); + let logged = LoggingExpression::new(timed, Box::new(ConsoleLogger)); + // When evaluate is called on the outermost decorator, the call flows through + // each layer. The order of wrapping matters. + let result = logged.evaluate(&HashMap::new()); + + /// 2. Composite Pattern + /// Because all nodes implement Expression, Decorator Pattern work seamlessly + /// with Composite Pattern trees. + /// The tree structure encodes operator precedence directly: + let multiply = Box::new(BinaryOperation::new( + Box::new(NumberExpression::new(3.0)), + Box::new(NumberExpression::new(4.0)), + Operator::Multiply, + )); + let add = Box::new(BinaryOperation::new( + Box::new(NumberExpression::new(2.0)), + multiply, + Operator::Add, + )); + /// Evaluating the tree is a single method call: + let variables = HashMap::new(); + println!("Expression: {}", add.to_string()); + match add.evaluate(&variables) { + Ok(result) => println!("Result: {}", result), + Err(error) => eprintln!("Error: {}", error), + } + + Ok(()) +} diff --git a/design_patterns_in_rust/good_calculator_2/src/number.rs b/design_patterns_in_rust/good_calculator_2/src/number.rs @@ -0,0 +1,77 @@ +use crate::NumberToken; + +#[derive(Debug, Clone, PartialEq)] +pub struct Number { + pub value: f64, + pub format: NumberFormat, +} + +impl Number { + pub fn format(&self) -> String { + match self.format { + NumberFormat::Decimal => format!("{}", self.value), + NumberFormat::Scientific => format!("{:e}", self.value), + NumberFormat::Engineering => { + // Engineering notation adjusts exponent to be multiple of 3 + let exp = self.value.abs().log10().floor(); + let adj_exp = (exp - exp % 3.0).floor(); + let coeff = self.value / 10_f64.powf(adj_exp); + format!("{}e{}", coeff, adj_exp) + } + } + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub enum NumberFormat { + #[default] + Decimal, + Scientific, + Engineering, +} + +#[derive(Debug, Clone, Default)] +pub enum AngleMode { + Degrees, + #[default] + Radians, +} + +#[derive(Clone)] +pub struct StandardNumber(pub f64); + +impl NumberToken for StandardNumber { + fn value(&self) -> f64 { + self.0 + } + + fn format(&self) -> String { + format!("{}", self.0) + } +} + +#[derive(Clone)] +pub struct ScientificNumber { + pub value: f64, + pub format: NumberFormat, +} + +/// Different factory families can have different internal representations +impl NumberToken for ScientificNumber { + fn value(&self) -> f64 { + self.value + } + + fn format(&self) -> String { + match self.format { + NumberFormat::Scientific => format!("{:e}", self.value), + NumberFormat::Engineering => { + let exp = self.value.abs().log10().floor(); + let adj_exp = (exp - exp % 3.0).floor(); + let coeff = self.value / 10_f64.powf(adj_exp); + format!("{}e{}", coeff, adj_exp) + } + _ => format!("{}", self.value), + } + } +} diff --git a/design_patterns_in_rust/good_calculator_2/src/operator.rs b/design_patterns_in_rust/good_calculator_2/src/operator.rs @@ -0,0 +1,186 @@ +use std::{collections::HashMap, f32::consts::FRAC_PI_2}; + +use crate::{Expression, OperatorToken, token::Function}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Operator { + Add, + Subtract, + Multiply, + Divide, + Power, + Root, + Factorial, +} + +#[derive(Clone)] +pub struct StandardOperator(pub Operator); + +impl OperatorToken for StandardOperator { + fn precedence(&self) -> u8 { + match self.0 { + Operator::Add | Operator::Subtract => 1, + Operator::Multiply | Operator::Divide => 2, + Operator::Power => 3, + Operator::Root | Operator::Factorial => 4, + } + } + + fn evaluate(&self, args: &[f64]) -> Result<f64, String> { + match self.0 { + Operator::Add => Ok(args[0] + args[1]), + Operator::Subtract => Ok(args[0] - args[1]), + Operator::Multiply => Ok(args[0] * args[1]), + Operator::Divide => { + if args[1] == 0.0 { + Err("Division by zero".to_string()) + } else { + Ok(args[0] / args[1]) + } + } + // ... other operators + _ => Err("Operation not supported in standard mode".to_string()), + } + } +} + +#[derive(Clone)] +pub enum ScientificOperator { + Basic(Operator), + Function(Function), +} + +impl OperatorToken for ScientificOperator { + fn precedence(&self) -> u8 { + todo!() + } + + fn evaluate(&self, args: &[f64]) -> Result<f64, String> { + todo!() + } +} + +/// Composite Pattern: this is a composite node, it holds two child expressions +/// and an operator. The children are `Box<dyn Expression>` trait objects, which +/// means each child can be any expression type: a number, a variable, another +/// binary operation, or even a decorated expression. +/// +/// We use `Box<dyn Expression>` rather than generic type parameters because +/// generic would make each `BinaryOperation` monomorphic (specialized to one +/// concrete type) over its children's types. +pub struct BinaryOperation { + pub left: Box<dyn Expression>, + pub right: Box<dyn Expression>, + pub operator: Operator, +} + +impl BinaryOperation { + pub fn new(left: Box<dyn Expression>, right: Box<dyn Expression>, operator: Operator) -> Self { + Self { + left, + right, + operator, + } + } + + fn operator_symbol(&self) -> &'static str { + match self.operator { + Operator::Add => "+", + Operator::Subtract => "-", + Operator::Multiply => "*", + Operator::Divide => "/", + Operator::Power => "^", + Operator::Root => todo!(), + Operator::Factorial => todo!(), + } + } +} + +impl Expression for BinaryOperation { + fn evaluate(&self, variables: &std::collections::HashMap<String, f64>) -> Result<f64, String> { + // Evaluates both children recursively + let l = self.left.evaluate(variables)?; + let r = self.right.evaluate(variables)?; + + match self.operator { + Operator::Add => Ok(l + r), + Operator::Subtract => Ok(l - r), + Operator::Multiply => Ok(l * r), + Operator::Divide if r == 0.0 => Err("Division by zero".to_string()), + Operator::Divide => Ok(l / r), + Operator::Power => Ok(l.powf(r)), + _ => Ok(l + r), + } + } + + fn to_string(&self) -> String { + let left_str = if self.left.precedence() < self.precedence() { + format!("({})", self.left.to_string()) + } else { + self.left.to_string() + }; + + let right_str = if self.right.precedence() < self.precedence() { + format!("({})", self.right.to_string()) + } else { + self.right.to_string() + }; + + format!("{} {} {}", left_str, self.operator_symbol(), right_str) + } + + fn precedence(&self) -> u8 { + match self.operator { + Operator::Add | Operator::Subtract => 1, + Operator::Multiply | Operator::Divide => 2, + Operator::Power => 3, + Operator::Root => todo!(), + Operator::Factorial => todo!(), + } + } +} + +/// Like BinaryOperation, FunctionCall demonstrates the recursive nature of +/// the Composite pattern: the argument can itself be an arbitrarily complex +/// expression tree, and the uniform Expression interface handles any depth +/// of nesting. +pub struct FunctionCall { + pub function: Function, + pub argument: Box<dyn Expression>, +} + +impl Expression for FunctionCall { + fn evaluate(&self, variables: &HashMap<String, f64>) -> Result<f64, String> { + let val = self.argument.evaluate(variables)?; + + match self.function { + Function::Sin => Ok(val.sin()), + Function::Cos => Ok(val.cos()), + Function::Tan => { + let hp = std::f64::consts::FRAC_PI_2; + if (val - hp).abs() % std::f64::consts::PI < 1e-10 { + Err("Targent undefined at this value".into()) + } else { + Ok(val.tan()) + } + } + Function::Sqrt if val < 0.0 => Err("Cannot take square root of negative number".into()), + Function::Sqrt => Ok(val.sqrt()), + } + } + + /// This method reconstructs the function call syntax for display purpose + fn to_string(&self) -> String { + let func_name = match self.function { + Function::Sqrt => "sqrt", + Function::Sin => "sin", + Function::Cos => "cos", + Function::Tan => "tan", + }; + format!("{}({})", func_name, self.argument.to_string()) + } + + fn precedence(&self) -> u8 { + 4 + } +} diff --git a/design_patterns_in_rust/good_calculator_2/src/token.rs b/design_patterns_in_rust/good_calculator_2/src/token.rs @@ -0,0 +1,51 @@ +use std::borrow::Cow; + +/// Instead of using class hierarchy with factory methods for each type, in Rust +/// we can take a more natural approach, using enums (which provide sum type +/// functionality) +/// +/// The Token enum is now generic over Number and Operator types to support +/// different factory implementations. +#[derive(Debug, Clone, PartialEq)] +pub enum Token<N, O> { + Number(N), + Operator(O), + Function(Function), + Variable(String), + OpenParen, + CloseParen, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Function { + Sqrt, + Sin, + Cos, + Tan, +} + +/// Constructors for the different Token enum types +impl<N, O> Token<N, O> { + pub fn function(func: Function) -> Self { + Self::Function(func) + } + + pub fn variable(name: impl Into<String>) -> Self { + Self::Variable(name.into()) + } +} + +/// Using Cow for Strings and Slices - Prototype Pattern +pub struct VariableToken<'a> { + name: Cow<'a, str>, + value: f64, +} + +impl<'a> VariableToken<'a> { + pub fn new(name: impl Into<Cow<'a, str>>, value: f64) -> Self { + Self { + name: name.into(), + value, + } + } +} diff --git a/design_patterns_in_rust/good_calculator_2/src/utilities.rs b/design_patterns_in_rust/good_calculator_2/src/utilities.rs @@ -0,0 +1,176 @@ +//! Decorator pattern + +use std::{cell::RefCell, collections::HashMap, time::Instant}; + +pub trait Expression { + fn evaluate(&self, variables: &HashMap<String, f64>) -> Result<f64, String>; + fn to_string(&self) -> String; + fn precedence(&self) -> u8 { + 0 + } +} + +// Leaf node for number values +#[derive(Debug, Clone)] +pub struct NumberExpression { + pub value: f64, +} + +impl NumberExpression { + pub fn new(value: f64) -> Self { + Self { value } + } +} + +impl Expression for NumberExpression { + fn evaluate(&self, _variables: &HashMap<String, f64>) -> Result<f64, String> { + Ok(self.value) + } + + fn to_string(&self) -> String { + format!("{}", self.value) + } +} + +/// Composites Pattern +pub struct VariableExpression { + pub name: String, +} + +impl Expression for VariableExpression { + fn evaluate(&self, variables: &HashMap<String, f64>) -> Result<f64, String> { + variables + .get(&self.name) + .copied() + .ok_or_else(|| format!("Undefined variable: {}", self.name)) + } + + fn to_string(&self) -> String { + self.name.clone() + } +} + +/// Logging decorator +/// +/// Notice that Logger is itself a trait object, which means we can swap in +/// different logging backends (console, file, network) without changing +/// the decorator +pub trait Logger { + fn log(&self, message: &str); +} + +pub struct ConsoleLogger; + +impl Logger for ConsoleLogger { + fn log(&self, message: &str) { + println!("[LOG] {}", message); + } +} + +/// This holds both the expression it decorates, and the logger it uses, +/// composing two independent abstractions +pub struct LoggingExpression { + inner: Box<dyn Expression>, + logger: Box<dyn Logger>, +} + +impl LoggingExpression { + pub fn new(inner: Box<dyn Expression>, logger: Box<dyn Logger>) -> Self { + Self { inner, logger } + } +} + +impl Expression for LoggingExpression { + fn evaluate(&self, variables: &HashMap<String, f64>) -> Result<f64, String> { + self.logger + .log(&format!("Evaluating: {}", self.inner.to_string())); + let result = self.inner.evaluate(variables); + match &result { + Ok(val) => self.logger.log(&format!("Result: {}", val)), + Err(err) => self.logger.log(&format!("Error: {}", err)), + } + result + } + + /// These delegation are critial, it makes the decorator transparent to any + /// code that doesn't care about logging + fn to_string(&self) -> String { + self.inner.to_string() + } + fn precedence(&self) -> u8 { + self.inner.precedence() + } +} + +/// The timing decorator pattern +pub struct TimingExpression { + inner: Box<dyn Expression>, +} + +impl TimingExpression { + pub fn new(inner: Box<dyn Expression>) -> Self { + Self { inner } + } +} + +/// The decorator captures the current time before delegating to the inner expression +impl Expression for TimingExpression { + fn evaluate(&self, variables: &HashMap<String, f64>) -> Result<f64, String> { + let start = Instant::now(); + let result = self.inner.evaluate(variables); + let duration = start.elapsed(); + println!("Evaluation took: {:?}", duration); + result + } + + /// These delegations are critial, it makes the decorator transparent to any + /// code that doesn't care about timing + fn to_string(&self) -> String { + self.inner.to_string() + } + fn precedence(&self) -> u8 { + self.inner.precedence() + } +} + +/// The caching decorator pattern with interior mutability +pub struct CachingExpression { + inner: Box<dyn Expression>, + // The RefCell<Option<f64>> allows us to mutate the cached value through + // a shared reference + last_result: RefCell<Option<f64>>, +} + +impl CachingExpression { + pub fn new(inner: Box<dyn Expression>) -> Self { + Self { + inner, + last_result: RefCell::new(None), + } + } + + pub fn invalidate_cache(&self) { + *self.last_result.borrow_mut() = None; + } +} + +impl Expression for CachingExpression { + fn evaluate(&self, variables: &HashMap<String, f64>) -> Result<f64, String> { + // Check if the result already cached + if let Some(result) = *self.last_result.borrow() { + return Ok(result); + } + // Cache the result + let result = self.inner.evaluate(variables)?; + *self.last_result.borrow_mut() = Some(result); + Ok(result) + } + + fn to_string(&self) -> String { + self.inner.to_string() + } + + fn precedence(&self) -> u8 { + self.inner.precedence() + } +} diff --git a/design_patterns_in_rust/index.md b/design_patterns_in_rust/index.md @@ -0,0 +1,84 @@ +--- +title: Design Patterns and Best Practices in Rust +--- + +Rust exercises following the book Design Patterns and Best Practices in Rust - +Evan Williams + +Learning by doing a few bad examples and a few good examples. + +## Bad Examples with Anti-Patterns in Rust using OOP + +#### Use Box<dyn Operand> dynamic dispatch everywhere, end up writing a lot of code to force OOP class and interface polymorphism: + +> [!info] Bad Calculator - Source Code: +> [exercises/design_patterns_in_rust/bad_calculator/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/design_patterns_in_rust/bad_calculator/src/main.rs) + +#### Leverage enums to perform the same functions but more concisely: + +> [!info] Not So Bad Calculator - Source Code: +> [exercises/design_patterns_in_rust/not_so_bad_calculator/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/design_patterns_in_rust/not_so_bad_calculator/src/main.rs) + +#### Use OO constructors and store more data inside enums to make it more generic that resulted in silent errors: + +> [!info] Slightly Worse Calculator - Source Code: +> [exercises/design_patterns_in_rust/slightly_worse_calculator/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/design_patterns_in_rust/slightly_worse_calculator/src/main.rs) + +#### Overuse enums and sub-enums to scale the code to more use cases end up with code that is very difficult to work with: + +> [!info] The Worst Calculator - Source Code: +> [exercises/design_patterns_in_rust/the_worst_calculator/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/design_patterns_in_rust/the_worst_calculator/src/main.rs) + +### Examples for ownership and lifetime management + +The advantages the better example offers that the bad examples don't: + +- Clear ownership semantics: Each component should own its data directly, with + no unnecessary indirection +- Explicit state management: The struct keeps related data together +- Better error handling: Use Result types to handle errors explicitly +- Thread-safety when needed: Instead of sprinkling Rc and RefCell throughout the + code, we provide a separate thread-safe wrapper when concurrent access is + required. +- Simpler mental model: We can reason about data flow more easily because + ownership and mutation are explicit +- Better performance: We avoid the overhead of reference counting and runtime + borrowing checks in the common case + +> [!info] Source Code: +> [exercises/design_patterns_in_rust/bad_calculator_2/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/design_patterns_in_rust/bad_calculator_2/src/main.rs) + +### These examples demonstrate the anti-pattern of using `unsafe` and `global statics` to fight the borrow checker and how they can be refactored to work with Rust's ownership system. + +- Separate concerns by phase: parse first, then process, then store. Each phase + should complete before the next begins. +- Use indices instead of references +- Let data flow downward +- Question whether you need interior mutability +- Keep mutable state local +- Trust the borrow checker's feedback + +> [!info] Source Code: +> [exercises/design_patterns_in_rust/bad_calculator_3/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/design_patterns_in_rust/bad_calculator_3/src/main.rs) + +## The Gang of Four design patterns + +### Creational patterns + +- Abstract Factory +- Builder Pattern +- Prototype Pattern + +> [!info] Source Code: +> [exercises/design_patterns_in_rust/good_calculator/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/design_patterns_in_rust/good_calculator/src/main.rs) + +### Structural patterns + +- Proxies, decorators, and adapters +- Facades +- Composites +- Flyweight +- The Bridge pattern + +> [!info] Source Code: +> [exercises/design_patterns_in_rust/good_calculator_2/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/design_patterns_in_rust/good_calculator_2/src/main.rs) diff --git a/design_patterns_in_rust/not_so_bad_calculator/Cargo.lock b/design_patterns_in_rust/not_so_bad_calculator/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "not_so_bad_calculator" +version = "0.1.0" diff --git a/design_patterns_in_rust/not_so_bad_calculator/Cargo.toml b/design_patterns_in_rust/not_so_bad_calculator/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "not_so_bad_calculator" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/design_patterns_in_rust/not_so_bad_calculator/src/main.rs b/design_patterns_in_rust/not_so_bad_calculator/src/main.rs @@ -0,0 +1,104 @@ +//! Using enums where they don't make sense! +//! We had so much difficulty and wrote so much code just to create +//! one of these in the bad_calculator. Let's use enums this time +//! for another bad example, albeit slightly better than the +//! Box<dyn Operand> approach in the other bad example + +use std::{ + io::{Write, stdin, stdout}, + process::exit, +}; + +enum Operand { + Value(f64), +} + +impl Operand { + fn evaluate(&self) -> f64 { + match self { + Operand::Value(v) => *v, + } + } +} + +enum Operator { + Addition { lhs: Operand, rhs: Operand }, + Subtraction { lhs: Operand, rhs: Operand }, + Multiplication { lhs: Operand, rhs: Operand }, + Division { lhs: Operand, rhs: Operand }, + Negation { operand: Operand }, +} + +impl Operator { + fn apply(&self) -> Operand { + let inner = match self { + Operator::Addition { lhs, rhs } => lhs.evaluate() + rhs.evaluate(), + Operator::Subtraction { lhs, rhs } => lhs.evaluate() - rhs.evaluate(), + Operator::Multiplication { lhs, rhs } => lhs.evaluate() * rhs.evaluate(), + Operator::Division { lhs, rhs } => lhs.evaluate() / rhs.evaluate(), + Operator::Negation { operand } => -operand.evaluate(), + }; + Operand::Value(inner) + } + + fn precedence(&self) -> u8 { + match self { + Operator::Addition { .. } | Operator::Subtraction { .. } => 0, + Operator::Multiplication { .. } | Operator::Division { .. } => 1, + Operator::Negation { .. } => 2, + } + } + + fn symbol(&self) -> char { + match self { + Operator::Addition { .. } => '+', + Operator::Subtraction { .. } => '-', + Operator::Multiplication { .. } => '*', + Operator::Division { .. } => '/', + Operator::Negation { .. } => '-', + } + } +} + +fn evaluate_expression(expression: &str) -> Result<String, String> { + let addition = Operator::Addition { + lhs: Operand::Value(2.0), + rhs: Operand::Value(3.0), + }; + let subtraction = Operator::Subtraction { + lhs: Operand::Value(5.0), + rhs: Operand::Value(1.0), + }; + let negation = Operator::Negation { + operand: Operand::Value(-7.0), + }; + + println!("Addition result: {}", addition.apply().evaluate()); + println!("Subtraction result: {}", subtraction.apply().evaluate()); + println!("Negation result: {}", negation.apply().evaluate()); + + Ok("Finished".to_string()) +} + +/// This is a project to demonstrate BAD practices and anti-patterns +/// in Rust in order to learn +fn main() { + let mut buf = String::new(); + loop { + print!("> "); + + stdout().flush().unwrap(); + + buf.clear(); + stdin().read_line(&mut buf).unwrap(); + + if buf.trim() == "exit" { + exit(0) + } + + match evaluate_expression(&buf) { + Ok(result) => println!("{result}"), + Err(error) => println!("Error: {error}"), + } + } +} diff --git a/design_patterns_in_rust/slightly_worse_calculator/Cargo.lock b/design_patterns_in_rust/slightly_worse_calculator/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "slightly_worse_calculator" +version = "0.1.0" diff --git a/design_patterns_in_rust/slightly_worse_calculator/Cargo.toml b/design_patterns_in_rust/slightly_worse_calculator/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "slightly_worse_calculator" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/design_patterns_in_rust/slightly_worse_calculator/src/main.rs b/design_patterns_in_rust/slightly_worse_calculator/src/main.rs @@ -0,0 +1,147 @@ +//! Using enums where they don't make sense! +//! We can certainly overuse enums and make the code worse, by +//! putting more states into each enum to make them store more data, +//! and by using a string type match arms that can introduce errors +//! silently, which the compiler won't even catch! + +use std::{ + io::{Write, stdin, stdout}, + process::exit, +}; + +enum Operand { + Value(f64), +} + +impl Operand { + fn evaluate(&self) -> f64 { + match self { + Operand::Value(v) => *v, + } + } +} + +enum Operator { + Addition { + lhs: Operand, + rhs: Operand, + precedence: u8, + symbol: char, + }, + Subtraction { + lhs: Operand, + rhs: Operand, + precedence: u8, + symbol: char, + }, + Multiplication { + lhs: Operand, + rhs: Operand, + precedence: u8, + symbol: char, + }, + Division { + lhs: Operand, + rhs: Operand, + precedence: u8, + symbol: char, + }, + Negation { + operand: Operand, + precedence: u8, + symbol: char, + }, +} + +impl Operator { + // Wrongly use Object Oriented constructor + fn new(operator_type: String, operand1: Operand, operand2: Option<Operand>) -> Self { + match operator_type.as_str() { + "add" => Operator::Addition { + lhs: operand1, + rhs: operand2.unwrap(), + precedence: 0, + symbol: '+', + }, + // notice the typo here, the compile wont catch it! + "sub" => Operator::Addition { + lhs: operand1, + rhs: operand2.unwrap(), + precedence: 0, + symbol: '-', + }, + // notice the typo here, the compile wont catch it! + "mul" => Operator::Division { + lhs: operand1, + rhs: operand2.unwrap(), + precedence: 1, + symbol: '*', + }, + "div" => Operator::Division { + lhs: operand1, + rhs: operand2.unwrap(), + precedence: 1, + symbol: '/', + }, + "neg" => Operator::Negation { + operand: operand1, + precedence: 2, + symbol: '-', + }, + _ => panic!("Unknown operator"), + } + } + + fn apply(&self) -> Operand { + let inner = match self { + Operator::Addition { lhs, rhs, .. } => lhs.evaluate() + rhs.evaluate(), + Operator::Subtraction { lhs, rhs, .. } => lhs.evaluate() - rhs.evaluate(), + Operator::Multiplication { lhs, rhs, .. } => lhs.evaluate() * rhs.evaluate(), + Operator::Division { lhs, rhs, .. } => lhs.evaluate() / rhs.evaluate(), + Operator::Negation { operand, .. } => -operand.evaluate(), + }; + Operand::Value(inner) + } +} + +fn evaluate_expression(expression: &str) -> Result<String, String> { + let addition = Operator::new( + "add".to_string(), + Operand::Value(2.0), + Some(Operand::Value(3.0)), + ); + let subtraction = Operator::new( + "sub".to_string(), + Operand::Value(5.0), + Some(Operand::Value(1.0)), + ); + let negation = Operator::new("neg".to_string(), Operand::Value(-7.0), None); + + println!("Addition result: {}", addition.apply().evaluate()); + println!("Subtraction result: {}", subtraction.apply().evaluate()); + println!("Negation result: {}", negation.apply().evaluate()); + + Ok("Finished".to_string()) +} + +/// This is a project to demonstrate BAD practices in Rust in order to learn +fn main() { + let mut buf = String::new(); + loop { + print!("> "); + + stdout().flush().unwrap(); + + buf.clear(); + stdin().read_line(&mut buf).unwrap(); + + if buf.trim() == "exit" { + exit(0) + } + + match evaluate_expression(&buf) { + Ok(result) => println!("{result}"), + Err(error) => println!("Error: {error}"), + } + } +} diff --git a/design_patterns_in_rust/the_worst_calculator/Cargo.lock b/design_patterns_in_rust/the_worst_calculator/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "the_worst_calculator" +version = "0.1.0" diff --git a/design_patterns_in_rust/the_worst_calculator/Cargo.toml b/design_patterns_in_rust/the_worst_calculator/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "the_worst_calculator" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/design_patterns_in_rust/the_worst_calculator/src/main.rs b/design_patterns_in_rust/the_worst_calculator/src/main.rs @@ -0,0 +1,118 @@ +//! Overuse enums in rust makes it very difficult to reason about + +use std::{ + io::{Write, stdin, stdout}, + ops::Range, + process::exit, + time::{Duration, Instant}, +}; + +enum Operand { + NumericValue(f64), + StringValue(String), + RangeValue(Range<usize>), + InstantValue(Instant), + DurationValue, +} + +// Sub-enums of enums... +// +enum ArithmeticOperator { + Addition { lhs: f64, rhs: f64 }, + Subtraction { lhs: f64, rhs: f64 }, + // ... +} + +enum TextOperator { + Concatenate { + lhs: String, + rhs: String, + }, + SubString { + operand: String, + bounds: Range<usize>, + }, + // ... +} + +enum DateOperator { + AddDays { lhs: Instant, rhs: Duration }, + SubtractDays { lhs: Instant, rhs: Instant }, + // ... +} + +enum Operator { + Arithmetic(ArithmeticOperator), + Text(TextOperator), + Date(DateOperator), +} + +impl ArithmeticOperator { + fn apply(&self) -> Operand { + match self { + ArithmeticOperator::Addition { lhs, rhs } => todo!(), + ArithmeticOperator::Subtraction { lhs, rhs } => todo!(), + } + } +} + +impl TextOperator { + fn apply(&self) -> Operand { + match self { + TextOperator::Concatenate { lhs, rhs } => todo!(), + TextOperator::SubString { operand, bounds } => todo!(), + } + } +} + +impl DateOperator { + fn apply(&self) -> Operand { + match self { + DateOperator::AddDays { lhs, rhs } => todo!(), + DateOperator::SubtractDays { lhs, rhs } => todo!(), + } + } +} + +/// Forcing OO hierarchy onto rust by using enums and sub-enums, +/// apply() and sub apply(), even though this could work, we have +/// made the relatively clean patterns very difficult to work with +/// +/// Instead of using OO hierarchies, we should use rust modules to +/// keep like types togther via the module system... +impl Operator { + fn apply(&self) -> Operand { + match self { + Operator::Arithmetic(arithmetic_operator) => arithmetic_operator.apply(), + Operator::Text(text_operator) => text_operator.apply(), + Operator::Date(date_operator) => date_operator.apply(), + } + } +} + +fn evaluate_expression(expression: &str) -> Result<Operand, Operand> { + todo!() +} + +/// This is a project to demonstrate BAD practices and anti-patterns +/// in Rust in order to learn +fn main() { + // let mut buf = Operand::new(); + + // loop { + // print!("> "); + // std::io::stdout().flush().unwrap(); + + // buf.clear(); + // std::io::stdin().read_line(&mut buf).unwrap(); + + // if buf.trim() == "exit" { + // exit(0) + // } + + // match evaluate_expression(&buf) { + // Ok(result) => println!("{result}"), + // Err(error) => println!("Error: {error}"), + // } + // } +} diff --git a/index.md b/index.md @@ -0,0 +1,5 @@ +--- +title: Exercises +--- + +Exercises in rust programming language for various software engineering topics diff --git a/my_vec/Cargo.lock b/my_vec/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "my_vec" +version = "0.1.0" diff --git a/my_vec/Cargo.toml b/my_vec/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "my_vec" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/my_vec/index.md b/my_vec/index.md @@ -0,0 +1,9 @@ +--- +title: "Exercise: My Vec" +tags: [exercise, rust, vector] +--- + +A minimal vector built from scratch. + +> [!info] Source Code: +> [exercises/my_vec](https://github.com/ling0x/notes/tree/main/content/exercises/my_vec) diff --git a/my_vec/src/main.rs b/my_vec/src/main.rs @@ -0,0 +1,35 @@ +// This project implements Vec from scratch (https://doc.rust-lang.org/nomicon/vec/vec.html) + +use std::{mem, ptr::NonNull}; + +/// Layout: +/// A Vec has three parts: a pointer to the allocation, the size of the allocation, +/// and the number of elements that have been initialized. +pub struct Vec<T> { + // NonNull is a wrapper around a raw pointer, which is covariant over T + // and is decalred to never be null. + ptr: NonNull<T>, + cap: usize, + len: usize, +} + +// Vec<T> is Send/Sync if T is Send/Sync +// (this produces the same results as using Unique<T>) +unsafe impl<T: Send> Send for Vec<T> {} +unsafe impl<T: Sync> Sync for Vec<T> {} + +impl<T> Vec<T> { + pub fn new() -> Self { + assert!(mem::size_of::<T>() != 0, "We're not ready to handle ZSTs"); + Vec { + // Initialize values that lazily allocate like Vec::new does + ptr: NonNull::dangling(), + len: 0, + cap: 0, + } + } +} + +fn main() { + println!("Hello, world!"); +} diff --git a/spinlock/Cargo.lock b/spinlock/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "spinlock" +version = "0.1.0" diff --git a/spinlock/Cargo.toml b/spinlock/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "spinlock" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/spinlock/index.md b/spinlock/index.md @@ -0,0 +1,11 @@ +--- +title: "Exercise: Spinlock" +tags: [exercise, rust, concurrency, memory-safety] +--- + +A from-scratch implementation of a [spinlock](memory_safety/spinlock.md) in Rust +using atomics and `UnsafeCell`. The goal is to understand how a mutex works at +the lowest level — without relying on the OS scheduler. + +> [!info] Source Code: +> [exercises/spinlock/src/main.rs](https://github.com/ling0x/notes/blob/main/content/exercises/spinlock/src/main.rs) diff --git a/spinlock/src/main.rs b/spinlock/src/main.rs @@ -0,0 +1,158 @@ +use std::{ + cell::UnsafeCell, + hint::spin_loop, + ops::{Deref, DerefMut}, + sync::atomic::{ + AtomicBool, + Ordering::{Acquire, Relaxed, Release}, + }, + thread, +}; + +/// A Spinlock is the simplest possible implementation of a mutex, its general form looks like this +/// ```rust +/// static LOCKED: AtomicBool = AtomicBool::new(false); +/// // 1. To grab a lock, we repeatedly execute compareandswap until it succeeds. +/// // The CPU “spins” in this very short loop. +/// while LOCKED.compare_and_swap(false, true, Ordering::Acquire) { +/// // 4. Spinning is wasteful, so we use an intrinsic to instruct the CPU to +/// // enter a low-power mode. +/// std::sync::atomic::spin_loop_hint(); +/// } +/// // 2. Only one thread at a time can be here. +/// /* Critical section */ +/// // 3. To release the lock, we do a single atomic store. +/// LOCKED.store(false, Ordering::Release); +/// ``` +/// Checkout the "Spinlock Considered Harmful" post: +/// https://matklad.github.io/2020/01/02/spinlocks-considered-harmful.html +/// +/// All we need is a single boolean that indicates whether it is locked or not. +pub struct SpinLock<T> { + /// QUESTION: What does an atomic bool does conceptually? + /// - Stores a true/false value that can be shared across threads without a mutex. + /// - Provides atomic operations like load, store, swap, compare_exchange, + /// and bitwise ops (fetch_or, fetch_and, fetch_not), each taking a memory + /// Ordering to control how operations are seen across threads. + /// - Uses CPU atomic instructions so an update is either fully seen or not seen at all by other threads; there is no partial write. + locked: AtomicBool, + + /// We need to have an exclusive reference (&mut T) to the data protected by the lock + /// The value field holds the generic over the type of data the lock protects + /// We use UnsafeCell for interior mutability + value: UnsafeCell<T>, +} + +/// A Safe Interface Using a Lock Guard +/// +/// Wrap the reference in a type that implements the Drop trait to do something +/// when it is dropped. +/// +/// The existence of a Guard means that the SpinLock has been locked. +pub struct Guard<'a, T> { + lock: &'a SpinLock<T>, +} + +/// In order to make the UnsafeCell to be shareable between threads, we need to +/// promise to the compiler that it is actually safe for our type to be shared +/// between threads. +unsafe impl<T> Sync for SpinLock<T> where T: Send {} + +/// Mutual Exclusion — the guarantee that only one thread can access the +/// protected data at any given moment. +/// +/// Spinlock Mechanism +impl<T> SpinLock<T> { + pub const fn new(value: T) -> Self { + Self { + locked: AtomicBool::new(false), + value: UnsafeCell::new(value), + } + } + + /// The lock method returns a Guard, such that the user isn't required to + /// write unsafe, unchecked code when using the lock to protect their data + pub fn lock(&self) -> Guard<'_, T> { + while self.locked.swap(true, Acquire) { + // Within the while loop, we use a spin loop hint, which emits a + // special CPU instruction that says “I’m in a tight busy‑wait loop; + // expect lots of repeated reads and no useful work.” This lets + // the core change how it treats that thread without involving + // the OS scheduler. + spin_loop(); + } + Guard { lock: self } + } + + /// We use acquire and release memory ordering to make sure that every + /// unlock() call establishes a happens-before relationship with the + /// lock() calls that follow. + /// + /// # Safety + /// + /// The &mut T from lock() must be gone! + /// (And no cheating by keeping reference to fields of that T around!) + pub unsafe fn unlock(&self) { + self.locked.store(false, Release); + } + + /// use a compare-and-exchange operation to atomically check if the boolean + /// is false and set it to true if that’s the case + pub fn cas(&self) { + while self + .locked + .compare_exchange_weak(false, true, Acquire, Relaxed) + .is_err() + { + spin_loop(); + } + } +} + +/// To make Guard<T> behave like an (exclusive) reference, we have to implement +/// the special Deref and DerefMut traits +impl<T> Deref for Guard<'_, T> { + type Target = T; + fn deref(&self) -> &T { + // # Safety: + // The very existence of this Guard + // guarantees we've exclusively locked the lock. + unsafe { &*self.lock.value.get() } + } +} + +impl<T> DerefMut for Guard<'_, T> { + fn deref_mut(&mut self) -> &mut T { + // # Safety: + // The very existence of this Guard + // guarantees we've exclusively locked the lock. + unsafe { &mut *self.lock.value.get() } + } +} + +/// Add our own implementation of Send and Sync with the right bounds to make sure +/// our Guard is only Sync if T is Sync (and Send if T is Send) +unsafe impl<T> Send for Guard<'_, T> where T: Send {} +unsafe impl<T> Sync for Guard<'_, T> where T: Sync {} + +/// Implement Drop for Guard, allowing us to the unsafe unlock method safe again +impl<T> Drop for Guard<'_, T> { + fn drop(&mut self) { + self.lock.locked.store(false, Release); + } +} + +fn main() { + let x = SpinLock::new(Vec::new()); + thread::scope(|s| { + s.spawn(|| x.lock().push(1)); + s.spawn(|| { + let mut g = x.lock(); + g.push(2); + g.push(2); + }); + }); + let g = x.lock(); + assert!(g.as_slice() == [1, 2, 2] || g.as_slice() == [2, 2, 1]); + println!("{:#?}", g.as_slice()); +} diff --git a/tokio-tutorial/index.md b/tokio-tutorial/index.md @@ -0,0 +1,11 @@ +--- +title: "Exercise: Tokio Tutorial — My Redis" +tags: [exercise, rust, async, tokio, networking] +--- + +A minimal Redis-like server and client built while following the +[Tokio tutorial](https://tokio.rs/tokio/tutorial). Covers async tasks, TCP +sockets, shared state, and Tokio's channel primitives. + +> [!info] Source Code: +> [exercises/tokio-tutorial/my-redis](https://github.com/ling0x/notes/tree/main/content/exercises/tokio-tutorial/my-redis) diff --git a/tokio-tutorial/my-redis/Cargo.lock b/tokio-tutorial/my-redis/Cargo.lock @@ -0,0 +1,1014 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atoi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c897df197d57c25b37df9d8fa2f93ddbfeee9ebd2264350ac79c8ec4b795885" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mini-redis" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528890a6b63c88c9bda173867128abe62c0f5050dd959f067aeea09e90439f33" +dependencies = [ + "async-stream", + "atoi", + "bytes", + "structopt", + "tokio", + "tokio-stream", + "tracing", + "tracing-futures", + "tracing-subscriber", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "my-redis" +version = "0.1.0" +dependencies = [ + "bytes", + "mini-redis", + "tokio", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.13", + "regex-syntax 0.8.8", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.8", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/tokio-tutorial/my-redis/Cargo.toml b/tokio-tutorial/my-redis/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "my-redis" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { version = "1", features = ["full"] } +bytes = "1" +mini-redis = "0.4" diff --git a/tokio-tutorial/my-redis/examples/hello-redis.rs b/tokio-tutorial/my-redis/examples/hello-redis.rs @@ -0,0 +1,23 @@ +use mini_redis::{Result, client}; + +#[tokio::main] +async fn main() -> Result<()> { + // Open a connection to the mini-redis address + // + // connect function asynchronously establishes a TCP connection with the + // specified remote address. Once the connection is established, a client + // handle is returned. Even though the operation is performed asynchronously, + // the code we write looks synchronous. The only indication that the operation + // is asynchronous is the .await operator. + let mut client = client::connect("127.0.0.1:6379").await?; + + // Set the key "hello" with value "world" + client.set("hello", "world".into()).await?; + + // Get key "hello" + let result = client.get("hello").await?; + + print!("got value from the server; result={result:?}"); + + Ok(()) +} diff --git a/tokio-tutorial/my-redis/index.md b/tokio-tutorial/my-redis/index.md @@ -0,0 +1,76 @@ +--- +title: Tokio Tutorial +--- + +https://tokio.rs/tokio/tutorial + +## Four types of tokio channels: + +### oneshot channel + +The oneshot channel supports sending a single value from a single producer to a +single consumer. This channel is usually used to send the result of a +computation to a waiter. + +### mpsc channel + +The mpsc channel supports sending many values from many producers to a single +consumer. This channel is often used to send work to a task or to receive the +result of many computations. + +### broadcast channel + +The broadcast channel supports sending many values from many producers to many +consumers. Each consumer will receive each value. This channel can be used to +implement “fan out” style patterns common with pub / sub or “chat” systems. + +### watch channel + +The watch channel supports sending many values from many producers to many +consumers. However, only the most recent value is stored in the channel. +Consumers are notified when a new value is sent, but there is no guarantee that +consumers will see all values. + +https://tokio.rs/tokio/tutorial/channels + +https://docs.rs/tokio/1.49.0/tokio/sync/ + +## Concepts + +### Tokio Tasks + +Tasks are the unit of execution managed by the scheduler. Spawning the task +submits it to the Tokio scheduler, which then ensures that the task executes +when it has work to do. The spawned task may be executed on the same thread as +where it was spawned, or it may execute on a different runtime thread. The task +can also be moved between threads after being spawned. + +Tasks in Tokio are very lightweight. Under the hood, they require only a single +allocation and 64 bytes of memory. Applications should feel free to spawn +thousands, if not millions of tasks. + +https://tokio.rs/tokio/tutorial/spawning + +### Actors + +Nothing in the actor model requires that each actor is its own thread. To the +contrary, most actor systems suggest that there should be a large number of +actors, and so each actor should map to a task rather than a thread. After all, +actors require exclusive access to their wrapped resources only when they +execute, and do not care whether they are on a thread of their own or not. In +fact, very frequently, the actor model is used in conjunction with the worker +pool model—for example, an application that uses the multi- threaded +asynchronous runtime Tokio can spawn an asynchronous task for each actor, and +Tokio will then make the execution of each actor a job in its worker pool. Thus, +the execution of a given actor may move from thread to thread in the worker pool +as the actor yields and resumes, but every time the actor executes it maintains +exclusive access to its wrapped resource. + +(Excerpt from Rust for Rustaceans) + +## Examples + +Beginner's Guide to Concurrent Programming: Coding a Multithreaded Chat Server +using Tokio: + +https://github.com/pretzelhammer/rust-blog/blob/master/posts/chat-server.md diff --git a/tokio-tutorial/my-redis/src/bin/client.rs b/tokio-tutorial/my-redis/src/bin/client.rs @@ -0,0 +1,113 @@ +use bytes::Bytes; +use mini_redis::client; +use tokio::sync::{mpsc, oneshot}; + +/// Multiple different commands are multiplexed over a single channel. +#[derive(Debug)] +enum Command { + Get { + key: String, + resp: Responder<Option<Bytes>>, + }, + Set { + key: String, + val: Bytes, + resp: Responder<()>, + }, +} + +/// Provided by the requester and used by the manager task to send the command +/// response back to the requester +type Responder<T> = oneshot::Sender<mini_redis::Result<T>>; + +#[tokio::main] +async fn main() { + // The mpsc channel supports sending many values from many producers to + // a single consumer + // + // Create a new channel with a capacity of at most 32. + // It returns two values: a sender and a receiver + // + // tx and rx: + // This naming comes from electronics and networking, where signal lines + // are often labeled TX (transmit) and RX (receive), and Rust’s channel + // examples follow the same tradition to indicate which end sends and which + // end receives. + let (tx, mut rx) = mpsc::channel(32); + + // Sending from multiple tasks is done by cloning the Sender + let tx2 = tx.clone(); + + // Spawn a task that processes messages from the channel. + // First, a client connection is established to Redis. + // Then, received commands are issued via the Redis connection. + // + // The `move` keyword is used to **move** ownership of `rx` into the task. + let manager = tokio::spawn(async move { + // Establish a connection to the server + let mut client = client::connect("127.0.0.1:6379").await.unwrap(); + + // Start receiving messages + while let Some(cmd) = rx.recv().await { + match cmd { + Command::Get { key, resp } => { + let res = client.get(&key).await; + + // Calling send on oneshot::Sender completes immediately + // and does not require an .await. This is because send on + // a oneshot channel will always fail or succeed immediately + // without any form of waiting. + let _ = resp.send(res); + } + Command::Set { key, val, resp } => { + let res = client.set(&key, val).await; + + // Calling send on oneshot::Sender completes immediately + // and does not require an .await. This is because send on + // a oneshot channel will always fail or succeed immediately + // without any form of waiting. + let _ = resp.send(res); + } + } + } + }); + + // Both messages are sent to the single Receiver handle. + // It is not possible to clone the receiver of an mpsc channel. + let t1 = tokio::spawn(async move { + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::Get { + key: "foo".to_string(), + resp: resp_tx, + }; + + // Send the Get request + tx.send(cmd).await.unwrap(); + + // Await the response + let res = resp_rx.await; + println!("GOT = {:?}", res); + }); + + let t2 = tokio::spawn(async move { + let (resp_tx, resp_rx) = oneshot::channel(); + let cmd = Command::Set { + key: "foo".to_string(), + val: "bar".into(), + resp: resp_tx, + }; + + // Send the SET request + tx2.send(cmd).await.unwrap(); + + // Await the response + let res = resp_rx.await; + println!("GOT = {:?}", res); + }); + + // At the bottom of the main function, we .await the join handles to ensure + // the commands fully complete before the process exits. + t1.await.unwrap(); + t2.await.unwrap(); + manager.await.unwrap(); +} diff --git a/tokio-tutorial/my-redis/src/bin/server.rs b/tokio-tutorial/my-redis/src/bin/server.rs @@ -0,0 +1,74 @@ +use bytes::Bytes; +use mini_redis::{Connection, Frame}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tokio::net::{TcpListener, TcpStream}; + +// Using Arc allows the HashMap to be referenced concurrently from many tasks, +// potentially running on many threads. Throughout Tokio, the term handle is +// used to reference a value that provides access to some shared state. +// +// Use std::sync::Mutex instead of tokio::sync::Mutex +type Db = Arc<Mutex<HashMap<String, Bytes>>>; + +#[tokio::main] +async fn main() { + // Bind the listener to the address + let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap(); + + println!("Listening"); + + // The HashMap will be shared across many tasks and potentially many threads. + // To support this, it is wrapped in Arc<Mutex<_>> + let db = Arc::new(Mutex::new(HashMap::new())); + + loop { + // The second item contains the IP and port of the new connection. + let (socket, _) = listener.accept().await.unwrap(); + + // Clone the handle to the hash map + let db = db.clone(); + + println!("Accepted"); + + // A new task is spawned for each inbound socket. The socket is + // moved to the new task and processed there. + tokio::spawn(async move { + process(socket, db).await; + }); + } +} + +/// Process function handles incoming commands. It uses a HashMap to store values. +/// SET commands will insert into the HashMap and GET values will load them. +/// Additionally, we will use a loop to accept more than one command per connection. +async fn process(socket: TcpStream, db: Db) { + use mini_redis::Command::{self, Get, Set}; + + // Connection, provided by `mini-redis`, handles parsing frames from the socket + let mut connection = Connection::new(socket); + + // Use `read_frame` to receive a command from the connection. + while let Some(frame) = connection.read_frame().await.unwrap() { + let response = match Command::from_frame(frame).unwrap() { + Set(cmd) => { + let mut db = db.lock().unwrap(); + // The value is stored as Vec<u8> + db.insert(cmd.key().to_string(), cmd.value().clone()); + Frame::Simple("OK".to_string()) + } + Get(cmd) => { + let db = db.lock().unwrap(); + if let Some(value) = db.get(cmd.key()) { + Frame::Bulk(value.clone()) + } else { + Frame::Null + } + } + cmd => panic!("unimplemented {cmd:?}"), + }; + + // Write the response to the client + connection.write_frame(&response).await.unwrap(); + } +} diff --git a/tokio-tutorial/my-redis/src/connection.rs b/tokio-tutorial/my-redis/src/connection.rs @@ -0,0 +1,132 @@ +use bytes::{Buf, BytesMut}; +use mini_redis::{Frame, Result, frame::Error::Incomplete}; +use std::io::Cursor; +use tokio::{ + io::{self, AsyncReadExt, AsyncWriteExt, BufWriter}, + net::TcpStream, +}; + +pub struct Connection { + stream: BufWriter<TcpStream>, + buffer: BytesMut, +} + +impl Connection { + pub fn new(stream: TcpStream) -> Connection { + Connection { + stream: BufWriter::new(stream), + // Allocate the buffer with 4kb of capacity. + buffer: BytesMut::with_capacity(4096), + } + } + + /// Read a frame from the connection + /// + /// Returns `None` if EOF is reached + pub async fn read_frame(&mut self) -> Result<Option<Frame>> { + loop { + // Attempt to parse a frame from the buffered data. If + // enough data has been buffered, the frame is + // returned. + if let Some(frame) = self.parse_frame()? { + return Ok(Some(frame)); + } + + // There is not enough buffered data to read a frame. + // Attempt to read more data from the socket. + // + // On success, the number of bytes is returned. `0` + // indicates "end of stream". + if 0 == self.stream.read_buf(&mut self.buffer).await? { + // The remote closed the connection. For this to be + // a clean shutdown, there should be no data in the + // read buffer. If there is, this means that the + // peer closed the socket while sending a frame. + if self.buffer.is_empty() { + return Ok(None); + } else { + return Err("connection reset by peer".into()); + } + } + } + } + + fn parse_frame(&mut self) -> Result<Option<Frame>> { + // Create the `T: Buf` type + let mut buf = Cursor::new(&self.buffer[..]); + + // Check whether a full frame is available + match Frame::check(&mut buf) { + Ok(_) => { + // Get the bytes length of the frame + let len = buf.position() as usize; + + // Reset the internal cursor for the call to `parse`. + buf.set_position(0); + + // Parse the frame + let frame = Frame::parse(&mut buf)?; + + // Discard the frame from the buffer + self.buffer.advance(len); + + // Return the frame to the caller + Ok(Some(frame)) + } + Err(Incomplete) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// Write a frame to the connection + pub async fn write_frame(&mut self, frame: &Frame) -> io::Result<()> { + match frame { + Frame::Simple(val) => { + self.stream.write_u8(b'+').await?; + self.stream.write_all(val.as_bytes()).await?; + self.stream.write_all(b"\r\n").await?; + } + Frame::Error(val) => { + self.stream.write_u8(b'-').await?; + self.stream.write_all(val.as_bytes()).await?; + self.stream.write_all(b"\r\n").await?; + } + Frame::Integer(val) => { + self.stream.write_u8(b':').await?; + self.write_decimal(*val).await?; + } + Frame::Bulk(val) => { + let len = val.len(); + + self.stream.write_u8(b'$').await?; + self.write_decimal(len as u64).await?; + self.stream.write_all(val).await?; + self.stream.write_all(b"\r\n").await?; + } + Frame::Null => { + self.stream.write_all(b"$-1\r\n").await?; + } + Frame::Array(frames) => unimplemented!(), + } + + self.stream.flush().await; + + Ok(()) + } + + /// Write a decimal frame to the stream + async fn write_decimal(&mut self, val: u64) -> io::Result<()> { + use std::io::Write; + + // Convert the value to a string + let mut buf = [0u8; 12]; + let mut buf = Cursor::new(&mut buf[..]); + write!(&mut buf, "{}", val)?; + + let pos = buf.position() as usize; + self.stream.write_all(&buf.get_ref()[..pos]).await?; + self.stream.write_all(b"\r\n").await?; + + Ok(()) + } +} diff --git a/tokio-tutorial/my-redis/src/frame.rs b/tokio-tutorial/my-redis/src/frame.rs diff --git a/tokio-tutorial/my-redis/src/lib.rs b/tokio-tutorial/my-redis/src/lib.rs @@ -0,0 +1,2 @@ +pub mod connection; +pub mod frame;