From d582e14a5f4d714e34cc43f05dfc49e796bad9d0 Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Tue, 23 May 2023 20:21:36 +0800 Subject: [PATCH 1/5] Add Concrete examples for newtype and compose design Add Concrete examples for newtype and compose design --- src/patterns/behavioural/newtype.md | 41 ++++------ src/patterns/structural/compose-structs.md | 94 ++++++++++++++-------- 2 files changed, 77 insertions(+), 58 deletions(-) diff --git a/src/patterns/behavioural/newtype.md b/src/patterns/behavioural/newtype.md index 9e9f0195..533da99e 100644 --- a/src/patterns/behavioural/newtype.md +++ b/src/patterns/behavioural/newtype.md @@ -18,41 +18,30 @@ This creates a new type, rather than an alias to a type (`type` items). ## Example ```rust,ignore -// Some type, not necessarily in the same module or even crate. -struct Foo { - //.. -} - -impl Foo { - // These functions are not present on Bar. - //.. -} - -// The newtype. -pub struct Bar(Foo); - -impl Bar { - // Constructor. - pub fn new( - //.. - ) -> Self { +use std::fmt::Display; - //.. +// Create Newtype Password to override the Display trait for String +struct Password (String); +impl Display for Password { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "****************") } - - //.. } fn main() { - let b = Bar::new(...); - - // Foo and Bar are type incompatible, the following do not type check. - // let f: Foo = b; - // let b: Bar = Foo { ... }; + let unsecured_password: String = "ThisIsMyPassword".to_string(); + let secured_password: Password = Password(unsecured_password.clone()); + println!("unsecond_password: {}", unsecured_password); + println!("secured_password: {}", secured_password); } ``` +```shell +unsecond_password: ThisIsMyPassword +secured_password: **************** +``` + ## Motivation The primary motivation for newtypes is abstraction. It allows you to share diff --git a/src/patterns/structural/compose-structs.md b/src/patterns/structural/compose-structs.md index 013f6cb7..8d7658c7 100644 --- a/src/patterns/structural/compose-structs.md +++ b/src/patterns/structural/compose-structs.md @@ -20,51 +20,81 @@ Here is a contrived example of where the borrow checker foils us in our plan to use a struct: ```rust -struct A { - f1: u32, - f2: u32, - f3: u32, +struct Database { + connection_string: String, + timeout: u32, + pool_size: u32, } -fn foo(a: &mut A) -> &u32 { &a.f2 } -fn bar(a: &mut A) -> u32 { a.f1 + a.f3 } +fn print_database(database: &Database) { + println!("Connection string: {}", database.connection_string); + println!("Timeout: {}", database.timeout); + println!("Pool size: {}", database.pool_size); +} + +fn main() { + let mut db = Database { + connection_string: "initial string".to_string(), + timeout: 30, + pool_size: 100, + }; -fn baz(a: &mut A) { - // The later usage of x causes a to be borrowed for the rest of the function. - let x = foo(a); - // Borrow checker error: - // let y = bar(a); // ~ ERROR: cannot borrow `*a` as mutable more than once - // at a time - println!("{}", x); + let connection_string = &mut db.connection_string; + print_database(&db); // Immutable borrow of `db` happens here + *connection_string = "new string".to_string(); // Mutable borrow is used here } ``` -We can apply this design pattern and refactor `A` into two smaller structs, thus -solving the borrow checking issue: +We can apply this design pattern and refactor `Database` into three smaller +structs, thus solving the borrow checking issue: ```rust -// A is now composed of two structs - B and C. -struct A { - b: B, - c: C, +// Database is now composed of three structs - ConnectionString, Timeout and PoolSize. +// Let's decompose it into smaller structs +#[derive(Clone)] +struct ConnectionString { + value: String, } -struct B { - f2: u32, + +struct Timeout { + value: u32, } -struct C { - f1: u32, - f3: u32, + +struct PoolSize { + value: u32, } -// These functions take a B or C, rather than A. -fn foo(b: &mut B) -> &u32 { &b.f2 } -fn bar(c: &mut C) -> u32 { c.f1 + c.f3 } +// We then compose these smaller structs back into `Database` +struct Database { + connection_string: ConnectionString, + timeout: Timeout, + pool_size: PoolSize, +} + +// print_database can then take ConnectionString, Timeout and Poolsize struct instead +fn print_database(connection_str: ConnectionString, + timeout: Timeout, + pool_size: PoolSize) { + println!("Connection string: {}", connection_str.value); + println!("Timeout: {}", timeout.value); + println!("Pool size: {}", pool_size.value); +} -fn baz(a: &mut A) { - let x = foo(&mut a.b); - // Now it's OK! - let y = bar(&mut a.c); - println!("{}", x); +fn main() { + // Initialize the three structs + let connection_string = ConnectionString { value: "localhost".to_string() }; + let timeout = Timeout { value: 30 }; + let pool_size = PoolSize { value: 100 }; + + let mut db = Database { + connection_string, + timeout, + pool_size, + }; + + let connection_string = &mut db.connection_string; + print_database(connection_string.clone(), db.timeout, db.pool_size); + *connection_string = ConnectionString{ value:"new string".to_string() }; } ``` From 19c13fc88b0fd8c765ff95e788872b430020b9c0 Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Wed, 24 May 2023 18:37:31 +0800 Subject: [PATCH 2/5] Fix Typo & CI --- src/patterns/behavioural/newtype.md | 8 ++++---- src/patterns/structural/compose-structs.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/patterns/behavioural/newtype.md b/src/patterns/behavioural/newtype.md index 533da99e..736eb69c 100644 --- a/src/patterns/behavioural/newtype.md +++ b/src/patterns/behavioural/newtype.md @@ -17,7 +17,7 @@ This creates a new type, rather than an alias to a type (`type` items). ## Example -```rust,ignore +```rust use std::fmt::Display; // Create Newtype Password to override the Display trait for String @@ -32,13 +32,13 @@ impl Display for Password { fn main() { let unsecured_password: String = "ThisIsMyPassword".to_string(); let secured_password: Password = Password(unsecured_password.clone()); - println!("unsecond_password: {}", unsecured_password); - println!("secured_password: {}", secured_password); + println!("unsecured_password: {unsecured_password}"); + println!("secured_password: {secured_password}"); } ``` ```shell -unsecond_password: ThisIsMyPassword +unsecured_password: ThisIsMyPassword secured_password: **************** ``` diff --git a/src/patterns/structural/compose-structs.md b/src/patterns/structural/compose-structs.md index 8d7658c7..4306821b 100644 --- a/src/patterns/structural/compose-structs.md +++ b/src/patterns/structural/compose-structs.md @@ -41,7 +41,7 @@ fn main() { let connection_string = &mut db.connection_string; print_database(&db); // Immutable borrow of `db` happens here - *connection_string = "new string".to_string(); // Mutable borrow is used here + // *connection_string = "new string".to_string(); // Mutable borrow is used here } ``` From dbd5adf41203485690cda70225002bfe2da849ec Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Thu, 25 May 2023 17:41:05 +0800 Subject: [PATCH 3/5] Update compose-structs.md --- src/patterns/structural/compose-structs.md | 30 ++++++++++------------ 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/patterns/structural/compose-structs.md b/src/patterns/structural/compose-structs.md index 4306821b..0d30ad6c 100644 --- a/src/patterns/structural/compose-structs.md +++ b/src/patterns/structural/compose-structs.md @@ -51,18 +51,14 @@ structs, thus solving the borrow checking issue: ```rust // Database is now composed of three structs - ConnectionString, Timeout and PoolSize. // Let's decompose it into smaller structs -#[derive(Clone)] -struct ConnectionString { - value: String, -} +#[derive(Debug, Clone)] +struct ConnectionString(String); -struct Timeout { - value: u32, -} +#[derive(Debug)] +struct Timeout(u32); -struct PoolSize { - value: u32, -} +#[derive(Debug)] +struct PoolSize(u32); // We then compose these smaller structs back into `Database` struct Database { @@ -75,16 +71,16 @@ struct Database { fn print_database(connection_str: ConnectionString, timeout: Timeout, pool_size: PoolSize) { - println!("Connection string: {}", connection_str.value); - println!("Timeout: {}", timeout.value); - println!("Pool size: {}", pool_size.value); + println!("Connection string: {:?}", connection_str); + println!("Timeout: {:?}", timeout); + println!("Pool size: {:?}", pool_size); } fn main() { // Initialize the three structs - let connection_string = ConnectionString { value: "localhost".to_string() }; - let timeout = Timeout { value: 30 }; - let pool_size = PoolSize { value: 100 }; + let connection_string = ConnectionString("localhost".to_string()); + let timeout = Timeout(30); + let pool_size = PoolSize(100); let mut db = Database { connection_string, @@ -94,7 +90,7 @@ fn main() { let connection_string = &mut db.connection_string; print_database(connection_string.clone(), db.timeout, db.pool_size); - *connection_string = ConnectionString{ value:"new string".to_string() }; + *connection_string = ConnectionString("new string".to_string()); } ``` From 68b566a0d632005fdce699d97b81a1ee61a56f82 Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Thu, 25 May 2023 15:41:23 +0200 Subject: [PATCH 4/5] Fix some TODOs in compose structs --- src/patterns/behavioural/newtype.md | 2 +- src/patterns/structural/compose-structs.md | 40 +++++++++------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/patterns/behavioural/newtype.md b/src/patterns/behavioural/newtype.md index 736eb69c..fa551382 100644 --- a/src/patterns/behavioural/newtype.md +++ b/src/patterns/behavioural/newtype.md @@ -21,7 +21,7 @@ This creates a new type, rather than an alias to a type (`type` items). use std::fmt::Display; // Create Newtype Password to override the Display trait for String -struct Password (String); +struct Password(String); impl Display for Password { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/patterns/structural/compose-structs.md b/src/patterns/structural/compose-structs.md index 0d30ad6c..62fd30ed 100644 --- a/src/patterns/structural/compose-structs.md +++ b/src/patterns/structural/compose-structs.md @@ -1,6 +1,4 @@ -# Compose structs together for better borrowing - -TODO - this is not a very snappy name +# Struct decomposition for independent borrowing ## Description @@ -41,7 +39,8 @@ fn main() { let connection_string = &mut db.connection_string; print_database(&db); // Immutable borrow of `db` happens here - // *connection_string = "new string".to_string(); // Mutable borrow is used here + // *connection_string = "new string".to_string(); // Mutable borrow is used + // here } ``` @@ -54,10 +53,10 @@ structs, thus solving the borrow checking issue: #[derive(Debug, Clone)] struct ConnectionString(String); -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] struct Timeout(u32); -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] struct PoolSize(u32); // We then compose these smaller structs back into `Database` @@ -77,15 +76,11 @@ fn print_database(connection_str: ConnectionString, } fn main() { - // Initialize the three structs - let connection_string = ConnectionString("localhost".to_string()); - let timeout = Timeout(30); - let pool_size = PoolSize(100); - + // Initialize the Database with the three structs let mut db = Database { - connection_string, - timeout, - pool_size, + connection_string: ConnectionString("localhost".to_string()), + timeout: Timeout(30), + pool_size: PoolSize(100), }; let connection_string = &mut db.connection_string; @@ -96,21 +91,20 @@ fn main() { ## Motivation -TODO Why and where you should use the pattern +This pattern is most useful, when you have a struct that ended up with a lot of +fields that you want to borrow independently. Thus having a more flexible +behaviour in the end. ## Advantages -Lets you work around limitations in the borrow checker. - -Often produces a better design. +Decomposition of structs lets you work around limitations in the borrow checker. +And it often produces a better design. ## Disadvantages -Leads to more verbose code. - -Sometimes, the smaller structs are not good abstractions, and so we end up with -a worse design. That is probably a 'code smell', indicating that the program -should be refactored in some way. +It can lead to more verbose code. And sometimes, the smaller structs are not +good abstractions, and so we end up with a worse design. That is probably a +'code smell', indicating that the program should be refactored in some way. ## Discussion From 7ce2d5955b0a4631cba4bb82fc8c42e435b6636d Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Thu, 25 May 2023 15:43:45 +0200 Subject: [PATCH 5/5] Fix 404 link in new type --- src/patterns/behavioural/newtype.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patterns/behavioural/newtype.md b/src/patterns/behavioural/newtype.md index fa551382..d7b5a82e 100644 --- a/src/patterns/behavioural/newtype.md +++ b/src/patterns/behavioural/newtype.md @@ -97,4 +97,4 @@ with `Bar`. - [Type aliases](https://doc.rust-lang.org/stable/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases) - [derive_more](https://crates.io/crates/derive_more), a crate for deriving many builtin traits on newtypes. -- [The Newtype Pattern In Rust](https://www.worthe-it.co.za/blog/2020-10-31-newtype-pattern-in-rust.html) +- [The Newtype Pattern In Rust](https://web.archive.org/web/20230519162111/https://www.worthe-it.co.za/blog/2020-10-31-newtype-pattern-in-rust.html)