From 50779f66b1f4859c32bdb987cfa81f933310da41 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 16 Jun 2024 23:53:12 +0800 Subject: [PATCH 1/5] Draft code outline for Impl-side dependencies --- .gitignore | 5 ++ Cargo.lock | 25 ++++++ Cargo.toml | 7 ++ content/blanket-implementations.md | 9 ++- content/impl-side-dependencies.md | 121 ++++++++++++++++++++++++++++- src/lib.rs | 1 + 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index 7585238..612e8f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ book + + +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..db33fdc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cgp-patterns" +version = "0.1.0" +dependencies = [ + "itertools", +] + +[[package]] +name = "either" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a5587c3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "cgp-patterns" +version = "0.1.0" +edition = "2021" + +[dependencies] +itertools = "0.13.0" \ No newline at end of file diff --git a/content/blanket-implementations.md b/content/blanket-implementations.md index 72ffacc..26b7e0f 100644 --- a/content/blanket-implementations.md +++ b/content/blanket-implementations.md @@ -67,12 +67,19 @@ person.greet(); As shown above, we are able to call `person.greet()` without having a context-specific implementation of `CanGreet` for `Person`. +## Extension Traits + The use of blanket trait implementation is commonly found in many Rust libraries today. For example, [`Itertools`](https://docs.rs/itertools/latest/itertools/trait.Itertools.html) provides a blanket implementation for any context that implements `Iterator`. Another example is [`StreamExt`](https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html), which is implemented for any context that implements `Stream`. +Traits such as `Itertools` and `StreamExt` are sometimes known as _extension traits_. This is +because the purpose of the trait is to _extend_ the behavior of existing types, without having +to own the type or base traits. While the use of extension traits is a common use case for +blanket implementations, there are other ways we can make use of blanket implementations. + ## Overriding Blanket Implementations Traits containing blanket implementation are usually not meant to be implemented manually @@ -215,6 +222,6 @@ Although a context many define its own context-specific provider to override the provider, it would face other limitations such as not being able to implement other traits that may cause a conflict. -In practice, we consider that blanket implementations allow for _singular context-generic provider_ +In practice, we consider that blanket implementations allow for _a singule context-generic provider_ to be defined. In future chapters, we will look at how to relax the singular constraint, to make it possible to allow _multiple_ context-generic or context-specific providers to co-exist. \ No newline at end of file diff --git a/content/impl-side-dependencies.md b/content/impl-side-dependencies.md index 8824f93..0577372 100644 --- a/content/impl-side-dependencies.md +++ b/content/impl-side-dependencies.md @@ -1,2 +1,121 @@ +# Impl-side Dependencies + +When writing generic code, we often need to specify the trait bounds that +we would like to use with a generic type. However, when the trait bounds +involve traits that make use of blanket implementations, there are different +ways that we can specify the trait bounds. + +As an example, supposed that we want to define a generic `format_items` +function that format a list of items into string. Our generic function could +make use the method +[`Itertools::join`](https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.join) +to join the iterator. With that, we may want to define our generic function as follows: + +```rust +# extern crate core; +# extern crate itertools; + +use core::fmt::Display; +use itertools::Itertools; + +fn format_items(items: C) -> String +where + C: IntoIterator, + C::Item: Display, +{ + items.into_iter().join(", ") +} + +# assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); +``` + +The `format_items` above works generically over any type `C` that implements +`IntoIterator`. Additionally, to use `Itertools::join`, we also require `C::Item` +to implement `Display`. With the trait bounds in place, we can simply call +`items.into_iter().join(", ")` to format the items as a comma-separated string. + + +```rust +# extern crate core; +# extern crate itertools; +# +# use core::fmt::Display; +# use itertools::Itertools; +# +# fn format_items(items: C) -> String +# where +# C: IntoIterator, +# C::Item: Display, +# { +# items.into_iter().join(", ") +# } + +fn stringly_equals(items_a: C, items_b: C) -> bool +where + C: IntoIterator, + C::Item: Display, +{ + format_items(items_a) == format_items(items_b) +} +``` + + +```rust +# extern crate core; +# extern crate itertools; + +use core::fmt::Display; +use itertools::Itertools; + +pub trait CanFormatItems { + fn format_items(&self) -> String; +} + +impl CanFormatItems for Context +where + Context: for<'a> IntoIterator, +{ + fn format_items(&self) -> String + { + items.into_iter().join(", ") + } +} +``` + + + +```rust +# extern crate core; +# extern crate itertools; +# +# use core::fmt::Display; +# use itertools::Itertools; +# +# pub trait CanFormatItems { +# fn format_items(&self) -> String; +# } +# +# impl CanFormatItems for Context +# where +# Context: for<'a> IntoIterator, +# { +# fn format_items(&self) -> String +# { +# items.into_iter().join(", ") +# } +# } + +pub trait CanStringlyCompareItems { + fn stringly_equals(&self, other: &Self) -> bool; +} + +impl CanStringlyCompareItems for Context +where + Context: CanFormatItems, +{ + fn stringly_equals(&self, other: &Self) -> bool { + self.format_items() == other.format_items() + } +} +``` -# Impl-side Dependencies \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ + From 15605498c8023dabdce8aed2057538754cf2bfbf Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 17 Jun 2024 00:06:56 +0800 Subject: [PATCH 2/5] Fix errors and CI --- .github/workflows/build.yaml | 4 +++- content/impl-side-dependencies.md | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bb7fcfd..b47e0e8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -41,8 +41,10 @@ jobs: - name: Run tests run: | + cargo clean + cargo test nix shell .#mdbook -c \ - mdbook test + mdbook test -L target/debug/deps - name: Upload artifact uses: actions/upload-pages-artifact@v3 diff --git a/content/impl-side-dependencies.md b/content/impl-side-dependencies.md index 0577372..8a9bbbb 100644 --- a/content/impl-side-dependencies.md +++ b/content/impl-side-dependencies.md @@ -73,11 +73,12 @@ pub trait CanFormatItems { impl CanFormatItems for Context where - Context: for<'a> IntoIterator, + for<'a> &'a Context: IntoIterator, + for<'a> <&'a Context as IntoIterator>::Item: Display, { fn format_items(&self) -> String { - items.into_iter().join(", ") + self.into_iter().join(", ") } } ``` @@ -97,11 +98,12 @@ where # # impl CanFormatItems for Context # where -# Context: for<'a> IntoIterator, +# for<'a> &'a Context: IntoIterator, +# for<'a> <&'a Context as IntoIterator>::Item: Display, # { # fn format_items(&self) -> String # { -# items.into_iter().join(", ") +# self.into_iter().join(", ") # } # } From b615d027ecef1b0eae4ee558a0f346dc8b4641d8 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Sun, 16 Jun 2024 18:12:50 +0200 Subject: [PATCH 3/5] Update Nix --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 114ac83..d8ebd45 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1712696601, - "narHash": "sha256-puFPFSa/RC83JilUgB48/VL387eu2QN066Jv6X971LY=", + "lastModified": 1718276985, + "narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=", "owner": "nixos", "repo": "nixpkgs", - "rev": "062fc6cf99d809921ecef47317752fc92468e6ae", + "rev": "3f84a279f1a6290ce154c5531378acc827836fbb", "type": "github" }, "original": { From fc42c5b71f2177c0f71b675d98d2578e44c49cfc Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Tue, 18 Jun 2024 13:24:20 +0800 Subject: [PATCH 4/5] Add more example code --- content/impl-side-dependencies.md | 321 ++++++++++++++++++++++++++++-- 1 file changed, 302 insertions(+), 19 deletions(-) diff --git a/content/impl-side-dependencies.md b/content/impl-side-dependencies.md index 8a9bbbb..3cf5317 100644 --- a/content/impl-side-dependencies.md +++ b/content/impl-side-dependencies.md @@ -11,6 +11,46 @@ make use the method [`Itertools::join`](https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.join) to join the iterator. With that, we may want to define our generic function as follows: + +```rust +# extern crate core; +# extern crate itertools; + +use core::fmt::Display; +use itertools::Itertools; + +fn format_iter(mut items: I) -> String +where + I: Iterator, + I::Item: Display, +{ + items.join(", ") +} + +assert_eq!(format_iter(vec![1, 2, 3].into_iter()), "1, 2, 3"); +``` + + + +```rust +# extern crate core; +# extern crate itertools; + +use core::fmt::Display; +use itertools::Itertools; + +fn format_iter(mut items: I) -> String +where + I: Itertools, + I::Item: Display, +{ + items.join(", ") +} + +assert_eq!(format_iter(vec![1, 2, 3].into_iter()), "1, 2, 3"); +``` + + ```rust # extern crate core; # extern crate itertools; @@ -18,21 +58,60 @@ to join the iterator. With that, we may want to define our generic function as f use core::fmt::Display; use itertools::Itertools; +pub trait CanFormatIter { + fn format_iter(self) -> String; +} + +impl CanFormatIter for I +where + I: Itertools, + I::Item: Display, +{ + fn format_iter(mut self) -> String + { + self.join(", ") + } +} + +assert_eq!(vec![1, 2, 3].into_iter().format_iter(), "1, 2, 3"); +``` + + + + +```rust +# extern crate core; +# extern crate itertools; +# +# use core::fmt::Display; +# use itertools::Itertools; +# +# pub trait CanFormatIter { +# fn format_iter(self) -> String; +# } +# +# impl CanFormatIter for I +# where +# I: Itertools, +# I::Item: Display, +# { +# fn format_iter(mut self) -> String +# { +# self.join(", ") +# } +# } + fn format_items(items: C) -> String where C: IntoIterator, C::Item: Display, { - items.into_iter().join(", ") + items.into_iter().format_iter() } -# assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); +assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); ``` -The `format_items` above works generically over any type `C` that implements -`IntoIterator`. Additionally, to use `Itertools::join`, we also require `C::Item` -to implement `Display`. With the trait bounds in place, we can simply call -`items.into_iter().join(", ")` to format the items as a comma-separated string. ```rust @@ -42,30 +121,97 @@ to implement `Display`. With the trait bounds in place, we can simply call # use core::fmt::Display; # use itertools::Itertools; # -# fn format_items(items: C) -> String +# pub trait CanFormatIter { +# fn format_iter(self) -> String; +# } +# +# impl CanFormatIter for I # where -# C: IntoIterator, -# C::Item: Display, +# I: Itertools, +# I::Item: Display, # { -# items.into_iter().join(", ") +# fn format_iter(mut self) -> String +# { +# self.join(", ") +# } # } -fn stringly_equals(items_a: C, items_b: C) -> bool +fn format_items(items: C) -> String where C: IntoIterator, - C::Item: Display, + C::IntoIter: CanFormatIter, { - format_items(items_a) == format_items(items_b) + items.into_iter().format_iter() } + +assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); ``` ```rust # extern crate core; # extern crate itertools; +# +# use core::fmt::Display; +# use itertools::Itertools; +# +# pub trait CanFormatIter { +# fn format_iter(self) -> String; +# } +# +# impl CanFormatIter for I +# where +# I: Itertools, +# I::Item: Display, +# { +# fn format_iter(mut self) -> String +# { +# self.join(", ") +# } +# } -use core::fmt::Display; -use itertools::Itertools; +fn format_items(items: &C) -> String +where + for <'a> &'a C: IntoIterator, + for <'a> <&'a C as IntoIterator>::IntoIter: CanFormatIter, +{ + items.into_iter().format_iter() +} + +assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); +``` + + + +The `format_items` above works generically over any type `C` that implements +`IntoIterator`. Additionally, to use `Itertools::join`, we also require `C::Item` +to implement `Display`. With the trait bounds in place, we can simply call +`items.into_iter().join(", ")` to format the items as a comma-separated string. + + + + +```rust +# extern crate core; +# extern crate itertools; +# +# use core::fmt::Display; +# use itertools::Itertools; +# +# pub trait CanFormatIter { +# fn format_iter(self) -> String; +# } +# +# impl CanFormatIter for I +# where +# I: Itertools, +# I::Item: Display, +# { +# fn format_iter(mut self) -> String +# { +# self.join(", ") +# } +# } pub trait CanFormatItems { fn format_items(&self) -> String; @@ -74,16 +220,67 @@ pub trait CanFormatItems { impl CanFormatItems for Context where for<'a> &'a Context: IntoIterator, - for<'a> <&'a Context as IntoIterator>::Item: Display, + for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter, { fn format_items(&self) -> String { - self.into_iter().join(", ") + self.into_iter().format_iter() } } + +assert_eq!(vec![1, 2, 3].format_items(), "1, 2, 3"); ``` +```rust +# extern crate core; +# extern crate itertools; +# +# use core::fmt::Display; +# use itertools::Itertools; +# +# pub trait CanFormatIter { +# fn format_iter(self) -> String; +# } +# +# impl CanFormatIter for I +# where +# I: Itertools, +# I::Item: Display, +# { +# fn format_iter(mut self) -> String +# { +# self.join(", ") +# } +# } +# +# pub trait CanFormatItems { +# fn format_items(&self) -> String; +# } +# +# impl CanFormatItems for Context +# where +# for<'a> &'a Context: IntoIterator, +# for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter, +# { +# fn format_items(&self) -> String +# { +# self.into_iter().format_iter() +# } +# } + +fn stringly_equals(left: &Context, right: &Context) -> bool +where + Context: CanFormatItems, +{ + left.format_items() == right.format_items() +} + +assert_eq!(stringly_equals(&vec![1, 2, 3], &vec![1, 2, 4]), false); +``` + + + ```rust # extern crate core; @@ -92,6 +289,21 @@ where # use core::fmt::Display; # use itertools::Itertools; # +# pub trait CanFormatIter { +# fn format_iter(self) -> String; +# } +# +# impl CanFormatIter for I +# where +# I: Itertools, +# I::Item: Display, +# { +# fn format_iter(mut self) -> String +# { +# self.join(", ") +# } +# } +# # pub trait CanFormatItems { # fn format_items(&self) -> String; # } @@ -99,11 +311,11 @@ where # impl CanFormatItems for Context # where # for<'a> &'a Context: IntoIterator, -# for<'a> <&'a Context as IntoIterator>::Item: Display, +# for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter, # { # fn format_items(&self) -> String # { -# self.into_iter().join(", ") +# self.into_iter().format_iter() # } # } @@ -119,5 +331,76 @@ where self.format_items() == other.format_items() } } + +assert_eq!(vec![1, 2, 3].stringly_equals(&vec![1, 2, 4]), false); ``` + + + +```rust +# extern crate core; +# extern crate itertools; +# +# use core::fmt::Display; +# use itertools::Itertools; +# +# pub trait CanFormatIter { +# fn format_iter(self) -> String; +# } +# +# impl CanFormatIter for I +# where +# I: Itertools, +# I::Item: Display, +# { +# fn format_iter(mut self) -> String +# { +# self.join(", ") +# } +# } +# +# pub trait CanFormatItems { +# fn format_items(&self) -> String; +# } +# +# impl CanFormatItems for Context +# where +# for<'a> &'a Context: IntoIterator, +# for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter, +# { +# fn format_items(&self) -> String +# { +# self.into_iter().format_iter() +# } +# } +# +# pub trait CanStringlyCompareItems { +# fn stringly_equals(&self, other: &Self) -> bool; +# } +# +# impl CanStringlyCompareItems for Context +# where +# Context: CanFormatItems, +# { +# fn stringly_equals(&self, other: &Self) -> bool { +# self.format_items() == other.format_items() +# } +# } +# +pub trait CanAssertEqualImpliesStringlyEqual { + fn assert_equal_implies_stringly_equal(&self, other: &Self); +} + +impl CanAssertEqualImpliesStringlyEqual for Context +where + Context: Eq + CanStringlyCompareItems, +{ + fn assert_equal_implies_stringly_equal(&self, other: &Self) { + assert_eq!(self == other, self.stringly_equals(other)) + } +} + +vec![1, 2, 3].assert_equal_implies_stringly_equal(&vec![1, 2, 3]); +vec![1, 2, 3].assert_equal_implies_stringly_equal(&vec![1, 2, 4]); +``` From 657f7b01c939b18357d8eeca897f7d3aef325a5f Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Mon, 1 Jul 2024 00:04:12 +0800 Subject: [PATCH 5/5] Finish drafting impl-side dependencies --- content/impl-side-dependencies.md | 259 +++++++++++++++++++++--------- 1 file changed, 187 insertions(+), 72 deletions(-) diff --git a/content/impl-side-dependencies.md b/content/impl-side-dependencies.md index 3cf5317..ac7fba2 100644 --- a/content/impl-side-dependencies.md +++ b/content/impl-side-dependencies.md @@ -5,11 +5,11 @@ we would like to use with a generic type. However, when the trait bounds involve traits that make use of blanket implementations, there are different ways that we can specify the trait bounds. -As an example, supposed that we want to define a generic `format_items` -function that format a list of items into string. Our generic function could -make use the method +Supposed that we want to define a generic function that formats a list of items +into a comma-separated string. Our generic function could make use the [`Itertools::join`](https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.join) -to join the iterator. With that, we may want to define our generic function as follows: +to format an iterator. Our first attempt would be to define our generic function +as follows: ```rust @@ -19,25 +19,39 @@ to join the iterator. With that, we may want to define our generic function as f use core::fmt::Display; use itertools::Itertools; -fn format_iter(mut items: I) -> String +fn format_iter(mut iter: I) -> String where I: Iterator, I::Item: Display, { - items.join(", ") + iter.join(", ") } assert_eq!(format_iter(vec![1, 2, 3].into_iter()), "1, 2, 3"); ``` +The generic function `format_iter` takes a generic type `I` that implements +`Iterator`. Additionally, we require `I::Item` to implement `Display`. With both +constraints in place, we are able to call `Itertools::join` inside the generic +function to join the items using `", "` as separator. + +In the above example, we are able to use the method from `Itertools` on `I`, +even though we do not specify the constraint `I: Itertools` in our `where` clause. +This is made possible because the trait `Itertools` has a blanket implementation +on all types that implement `Iterator`, including the case when we do not know +the concrete type behind `I`. Additionally, the method `Itertools::join` requires +`I::Item` to implement `Display`, so we also include the constraint in our where clause. + +When using traits that have blanket implementation, we can also go the other way +and require `I` to implement `Itertools` instead of `Iterator`: ```rust # extern crate core; # extern crate itertools; - -use core::fmt::Display; -use itertools::Itertools; +# +# use core::fmt::Display; +# use itertools::Itertools; fn format_iter(mut items: I) -> String where @@ -50,33 +64,19 @@ where assert_eq!(format_iter(vec![1, 2, 3].into_iter()), "1, 2, 3"); ``` +By doing so, we make it explicit of the intention that we only care that +`I` implements `Itertools`, and hide the fact that we need `I` to also implement +`Iterator` in order to implement `Itertools`. -```rust -# extern crate core; -# extern crate itertools; - -use core::fmt::Display; -use itertools::Itertools; - -pub trait CanFormatIter { - fn format_iter(self) -> String; -} - -impl CanFormatIter for I -where - I: Itertools, - I::Item: Display, -{ - fn format_iter(mut self) -> String - { - self.join(", ") - } -} - -assert_eq!(vec![1, 2, 3].into_iter().format_iter(), "1, 2, 3"); -``` +## Constraint Leaks +At this point, we have defined our generic function `format_iter` with two constraints +in the `where` clause. When calling `format_iter` from another generic function, the +constraint would also be propagated to the caller. +As a demonstration, supposed that we want to define another generic function that +uses `format_iter` to format any type that implements `IntoIterator`, we would need +to also include the constraints needed by `format_iter` as follows: ```rust @@ -86,33 +86,56 @@ assert_eq!(vec![1, 2, 3].into_iter().format_iter(), "1, 2, 3"); # use core::fmt::Display; # use itertools::Itertools; # -# pub trait CanFormatIter { -# fn format_iter(self) -> String; -# } -# -# impl CanFormatIter for I +# fn format_iter(mut items: I) -> String # where # I: Itertools, # I::Item: Display, # { -# fn format_iter(mut self) -> String -# { -# self.join(", ") -# } +# items.join(", ") # } fn format_items(items: C) -> String where C: IntoIterator, - C::Item: Display, + C::IntoIter: Itertools, + ::Item: Display, { - items.into_iter().format_iter() + format_iter(items.into_iter()) } assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); ``` +When defining the generic function `format_items` above, we only really care +that the generic type `C` implements `IntoIterator`, and then pass `C::IntoIter` +to `format_iter`. However, because of the constraints specified by `format_iter`, +Rust also forces us to specify the same constraints in `format_items`, even if +we don't need the constraints directly. + +As we can see, the constraints specified in the `where` clause of `format_iter` +is a form of leaky abstraction, as it also forces generic consumers like +`format_items` to also know about the internal details of how `format_iter` +uses the iterator. + +The leaking of `where` constraints also makes it challenging to write highly +generic functions at a larger scale. The number of constraints could quickly +become unmanageable, if a high level generic function calls many low-level +generic functions that each has different constraints. +Furthermore, the repeatedly specified constraints become tightly coupled with +the concrete implementation of the low-level functions. For example, if +`format_iter` changed from using `Itertools::join` to other ways of formatting +the iterator, the constraints would become outdated and need to be changed +in `format_items`. + + +## Hiding Constraints with Traits and Blanket Implementations + +Using the techniques we learned from blanket implementations, there is a way to +hide the `where` constraints by redefining our generic functions as traits with +blanket implementations. + +We would first rewrite `format_iter` into a trait `CanFormatIter` as follows: ```rust # extern crate core; @@ -120,33 +143,39 @@ assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); # # use core::fmt::Display; # use itertools::Itertools; -# -# pub trait CanFormatIter { -# fn format_iter(self) -> String; -# } -# -# impl CanFormatIter for I -# where -# I: Itertools, -# I::Item: Display, -# { -# fn format_iter(mut self) -> String -# { -# self.join(", ") -# } -# } -fn format_items(items: C) -> String +pub trait CanFormatIter { + fn format_iter(self) -> String; +} + +impl CanFormatIter for I where - C: IntoIterator, - C::IntoIter: CanFormatIter, + I: Itertools, + I::Item: Display, { - items.into_iter().format_iter() + fn format_iter(mut self) -> String + { + self.join(", ") + } } -assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); +assert_eq!(vec![1, 2, 3].into_iter().format_iter(), "1, 2, 3"); ``` +The trait `CanFormatIter` is defined with a single method, `format_iter`, which +consumes `self` and return a `String`. The trait comes with a blanket implementation +for any type `I`, with the constraints that `I: Itertools` and `I::Item: Display`. +Following that, we have the same implementation as before, which calls `Itertools::join` +to format the iterator as a comma-separated string. By having a blanket implementation, +we signal that `CanFormatIter` is intended to be derived automatically, and that no +explicit implementation is required. + +It is worth noting that the constraints `I: Itertools` and `I::Item: Display` are only +present at the `impl` block, but not at the `trait` definition of `CanFormatIter`. +By doing so, we have effectively "hidden" the constraints inside the `impl` block, +and prevent it from leaking to its consumers. + +We can now refactor `format_items` to use `CanFormatIter` as follows: ```rust # extern crate core; @@ -170,10 +199,10 @@ assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); # } # } -fn format_items(items: &C) -> String +fn format_items(items: C) -> String where - for <'a> &'a C: IntoIterator, - for <'a> <&'a C as IntoIterator>::IntoIter: CanFormatIter, + C: IntoIterator, + C::IntoIter: CanFormatIter, { items.into_iter().format_iter() } @@ -181,15 +210,28 @@ where assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3"); ``` +In the new version of `format_items`, our `where` constraints are now simplified +to only require `C::IntoIter` to implement `CanFormatIter`. With that, we are +able to make it explicit that `format_items` needs `CanFormatIter` to be implemented, +but it doesn't matter _how_ it is implemented. +The reason this technique works is similar to how we used `Itertools` in our +previous examples. At the call site of the code that calls `format_items`, Rust +would see that the generic function requires `C::IntoIter` to implement +`CanFormatIter`. But at the same time, Rust also sees that `CanFormatIter` has +a blanket implementation. So if the constraints specified at the blanket +implementation are satisfied, Rust would automatically provide an implementation +of `CanFormatIter` to `format_items`, without the caller needing to know how +that is done. -The `format_items` above works generically over any type `C` that implements -`IntoIterator`. Additionally, to use `Itertools::join`, we also require `C::Item` -to implement `Display`. With the trait bounds in place, we can simply call -`items.into_iter().join(", ")` to format the items as a comma-separated string. +## Nested Constraints Hiding +Once we have seen in action how we can hide constraints behind the blanket `impl` +blocks of traits, there is no stopping for us to define more traits with blanket +implementations to hide even more constraints. +For instance, we could rewrite `format_items` into a `CanFormatItems` trait as follows: ```rust # extern crate core; @@ -231,6 +273,37 @@ where assert_eq!(vec![1, 2, 3].format_items(), "1, 2, 3"); ``` +We first define a `CanFormatItems` trait, with a method `format_items(&self)`. +Here, we make an improvement over the original function to allow a reference `&self`, +instead of an owned value `self`. This allows a container such as `Vec` to +not be consumed when we try to format its items, which would be unnecessarily +inefficient. + +Inside the blanket `impl` block for `CanFormatItems`, we define it to work with any +`Context` type, given that the generic `Context` type implements some constraints +with [_higher ranked trait bounds_ (HRTB)](https://doc.rust-lang.org/nomicon/hrtb.html). +While HRTB is an advanced subject on its own, the general idea is that we require +that any reference `&'a Context` with any lifetime `'a` implements `IntoIterator`. +This is so that when +[`IntoIterator::into_iter`](https://doc.rust-lang.org/std/iter/trait.IntoIterator.html#tymethod.into_iter) +is called, the `Self` type being consumed is the reference type `&'a Context`, +which is implicitly copyable, and thus allow the same context to be reused later +on at other places. + +Additionally, we require that `<&'a Context as IntoIterator>::IntoIter` implements +`CanFormatIter`, so that we can call its method on the produced iterator. Thanks to +the hiding of constraints by `CanFormatIter`, we can avoid specifying an overly verbose +constraint that the iterator item also needs to implement `Display`. + +Individually, the constraints hidden by `CanFormatIter` and `CanFormatItems` may +not look significant. But when combining together, we can see how isolating the +constraints help us better organize our code and make them cleaner. +In particular, we can now write generic functions that consume `CanFormatIter` +without having to understand all the indirect constraints underneath. + +To demonstrate, supposed that we want to compare two list of items and see +whether they have the same string representation. We can now define a +generic `stringly_equals` function as follows: ```rust # extern crate core; @@ -279,8 +352,14 @@ where assert_eq!(stringly_equals(&vec![1, 2, 3], &vec![1, 2, 4]), false); ``` +Our generic function `stringly_equals` can now be defined cleanly to work over +any `Context` type that implements `CanFormatItems`. In this case, the function +does not even need to be aware that `Context` needs to produce an iterator, with +its items implementing `Display`. - +Furthermore, instead of defining a generic function, we could instead use the +same programming technique and define a `CanStringlyCompareItems` trait +that does the same thing: ```rust # extern crate core; @@ -335,8 +414,20 @@ where assert_eq!(vec![1, 2, 3].stringly_equals(&vec![1, 2, 4]), false); ``` +For each new trait we layer on top, we can build higher level interfaces that +hide away lower level implementation details. When `CanStringlyCompareItems` is +used, the consumer is shielded away from knowing anything about the concrete +context, other than that two values are be compared by first being formatted +into strings. + +The example here may seem a bit stupid, but there are some practical use cases of +implementing comparing two values as strings. For instance, a serialization library +may want to use it inside tests to check whether two different values are serialized +into the same string. For such use case, we may want to define another trait to +help make such assertion during tests: + ```rust # extern crate core; @@ -404,3 +495,27 @@ where vec![1, 2, 3].assert_equal_implies_stringly_equal(&vec![1, 2, 3]); vec![1, 2, 3].assert_equal_implies_stringly_equal(&vec![1, 2, 4]); ``` + +The trait `CanAssertEqualImpliesStringlyEqual` provides a method that +takes two contexts of the same type, and assert that if both context +values are equal, then their string representation are also equal. +Inside the blanket `impl` block, we require that `Context` implements +`CanStringlyCompareItems`, as well as `Eq`. + +Thanks to the hiding of constraints, the trait `CanAssertEqualImpliesStringlyEqual` +can cleanly separate its direct dependencies, `Eq`, from the rest of the +indirect dependencies. + +## Dependency Injection + +The programming technique that we have introduced in this chapter is sometimes +known as _dependency injection_ in some other languages and programming paradigms. +The general idea is that the `impl` blocks are able to specify the dependencies +they need in the form of `where` constraints, and the Rust trait system automatically +helps us to resolve the dependencies at compile time. + +In context-generic programming, we think of constraints in the `impl` blocks not as +constraints, but more generally _dependencies_ that the concrete implementation needs. +Each time a new trait is defined, it serves as an interface for consumers to include +them as a dependency, but at the same time separates the declaration from the concrete +implementations. \ No newline at end of file