Skip to content

Add Concrete examples for newtype and compose design #368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 17 additions & 28 deletions src/patterns/behavioural/newtype.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,31 @@ 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);
```rust
use std::fmt::Display;

impl Bar {
// Constructor.
pub fn new(
//..
) -> Self {

//..
// 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!("unsecured_password: {unsecured_password}");
println!("secured_password: {secured_password}");
}
```

```shell
unsecured_password: ThisIsMyPassword
secured_password: ****************
```

## Motivation

The primary motivation for newtypes is abstraction. It allows you to share
Expand Down Expand Up @@ -108,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)
112 changes: 66 additions & 46 deletions src/patterns/structural/compose-structs.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# Compose structs together for better borrowing

TODO - this is not a very snappy name
# Struct decomposition for independent borrowing

## Description

Expand All @@ -20,71 +18,93 @@ 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 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);
fn main() {
let mut db = Database {
connection_string: "initial string".to_string(),
timeout: 30,
pool_size: 100,
};

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,
}
struct B {
f2: u32,
}
struct C {
f1: u32,
f3: u32,
// Database is now composed of three structs - ConnectionString, Timeout and PoolSize.
// Let's decompose it into smaller structs
#[derive(Debug, Clone)]
struct ConnectionString(String);

#[derive(Debug, Clone, Copy)]
struct Timeout(u32);

#[derive(Debug, Clone, Copy)]
struct PoolSize(u32);

// We then compose these smaller structs back into `Database`
struct Database {
connection_string: ConnectionString,
timeout: Timeout,
pool_size: PoolSize,
}

// 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 }
// 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);
println!("Timeout: {:?}", timeout);
println!("Pool size: {:?}", pool_size);
}

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 Database with the three structs
let mut db = Database {
connection_string: ConnectionString("localhost".to_string()),
timeout: Timeout(30),
pool_size: PoolSize(100),
};

let connection_string = &mut db.connection_string;
print_database(connection_string.clone(), db.timeout, db.pool_size);
*connection_string = ConnectionString("new string".to_string());
}
```

## 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

Expand Down