From 71b244b67313e3241a9dda91e103e30061415897 Mon Sep 17 00:00:00 2001 From: Peter Goodspeed-Niklaus Date: Fri, 3 May 2019 23:25:34 +0200 Subject: [PATCH 1/3] Add fizzy: an exercise teaching advanced generics and impl trait I implemented my version because it was late and I was bored. Then I realized I had something interesting, and turned it into an exercise. Closes #821. --- config.json | 12 ++ exercises/fizzy/.gitignore | 3 + exercises/fizzy/.meta/description.md | 10 ++ exercises/fizzy/.meta/metadata.yml | 4 + exercises/fizzy/Cargo.toml | 7 ++ exercises/fizzy/README.md | 92 +++++++++++++++ exercises/fizzy/example.rs | 169 +++++++++++++++++++++++++++ exercises/fizzy/src/lib.rs | 46 ++++++++ exercises/fizzy/tests/fizzy.rs | 125 ++++++++++++++++++++ 9 files changed, 468 insertions(+) create mode 100644 exercises/fizzy/.gitignore create mode 100644 exercises/fizzy/.meta/description.md create mode 100644 exercises/fizzy/.meta/metadata.yml create mode 100644 exercises/fizzy/Cargo.toml create mode 100644 exercises/fizzy/README.md create mode 100644 exercises/fizzy/example.rs create mode 100644 exercises/fizzy/src/lib.rs create mode 100644 exercises/fizzy/tests/fizzy.rs diff --git a/config.json b/config.json index abb52ac62..e636cf9ff 100644 --- a/config.json +++ b/config.json @@ -898,6 +898,18 @@ "move_semantics" ] }, + { + "slug": "fizzy", + "uuid": "6b209749-d4af-45c2-bbdc-27603ce6979f", + "core": false, + "unlocked_by": "luhn", + "difficulty": 7, + "topics": [ + "generics", + "impl_trait", + "iterators" + ] + }, { "slug": "roman-numerals", "uuid": "498be645-734a-49b7-aba7-aae1e051e1f0", diff --git a/exercises/fizzy/.gitignore b/exercises/fizzy/.gitignore new file mode 100644 index 000000000..2f88dbac5 --- /dev/null +++ b/exercises/fizzy/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock \ No newline at end of file diff --git a/exercises/fizzy/.meta/description.md b/exercises/fizzy/.meta/description.md new file mode 100644 index 000000000..69a28dfd8 --- /dev/null +++ b/exercises/fizzy/.meta/description.md @@ -0,0 +1,10 @@ +FizzBuzz is a children's game of counting through the integers. For each of them, if it's divisible by three, substitute the word "fizz"; if divisible by five, substitue "buzz"; if both, say both; if neither, say the number. It is not particularly difficult to implement, though it enjoyed some popularity for a time as [a quick way to tell whether entry-level programming applicants knew how to program _at all_](https://blog.codinghorror.com/why-cant-programmers-program/). + +It has since fallen somewhat into disfavor for that task, because applicants began memorizing FizzBuzz implementations instead of learning to program. + +We're going to do something more interesting than the basics: your task in this exercise is to implement FizzBuzz: + +- with fully-customizable rules about what numbers produce what words +- fully generic on a very restricted minimal trait set +- such that it works just as well for the Collatz Sequence as for steadily increasing numbers +- with convenient helpers to make its use ergonomic diff --git a/exercises/fizzy/.meta/metadata.yml b/exercises/fizzy/.meta/metadata.yml new file mode 100644 index 000000000..7dc926d4f --- /dev/null +++ b/exercises/fizzy/.meta/metadata.yml @@ -0,0 +1,4 @@ +--- +blurb: "Implement FizzBuzz using advanced generics" +source: "Peter Goodspeed-Niklaus" +source_url: "https://github.com/coriolinus/fizzy" diff --git a/exercises/fizzy/Cargo.toml b/exercises/fizzy/Cargo.toml new file mode 100644 index 000000000..3afb03529 --- /dev/null +++ b/exercises/fizzy/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "fizzy" +version = "0.0.0" +edition = "2018" + +[dependencies] + diff --git a/exercises/fizzy/README.md b/exercises/fizzy/README.md new file mode 100644 index 000000000..c405ef7ce --- /dev/null +++ b/exercises/fizzy/README.md @@ -0,0 +1,92 @@ +# Fizzy + +FizzBuzz is a children's game of counting through the integers. For each of them, if it's divisible by three, substitute the word "fizz"; if divisible by five, substitue "buzz"; if both, say both; if neither, say the number. It is not particularly difficult to implement, though it enjoyed some popularity for a time as [a quick way to tell whether entry-level programming applicants knew how to program _at all_](https://blog.codinghorror.com/why-cant-programmers-program/). + +It has since fallen somewhat into disfavor for that task, because applicants began memorizing FizzBuzz implementations instead of learning to program. + +We're going to do something more interesting than the basics: your task in this exercise is to implement FizzBuzz: + +- with fully-customizable rules about what numbers produce what words +- fully generic on a very restricted minimal trait set +- such that it works just as well for the Collatz Sequence as for steadily increasing numbers +- with convenient helpers to make its use ergonomic + +## Rust Installation + +Refer to the [exercism help page][help-page] for Rust installation and learning +resources. + +## Writing the Code + +Execute the tests with: + +```bash +$ cargo test +``` + +All but the first test have been ignored. After you get the first test to +pass, open the tests source file which is located in the `tests` directory +and remove the `#[ignore]` flag from the next test and get the tests to pass +again. Each separate test is a function with `#[test]` flag above it. +Continue, until you pass every test. + +If you wish to run all tests without editing the tests source file, use: + +```bash +$ cargo test -- --ignored +``` + +To run a specific test, for example `some_test`, you can use: + +```bash +$ cargo test some_test +``` + +If the specific test is ignored use: + +```bash +$ cargo test some_test -- --ignored +``` + +To learn more about Rust tests refer to the [online test documentation][rust-tests] + +Make sure to read the [Modules](https://doc.rust-lang.org/book/ch07-02-modules-and-use-to-control-scope-and-privacy.html) chapter if you +haven't already, it will help you with organizing your files. + +## Further improvements + +After you have solved the exercise, please consider using the additional utilities, described in the [installation guide](https://exercism.io/tracks/rust/installation), to further refine your final solution. + +To format your solution, inside the solution directory use + +```bash +cargo fmt +``` + +To see, if your solution contains some common ineffective use cases, inside the solution directory use + +```bash +cargo clippy --all-targets +``` + +## Submitting the solution + +Generally you should submit all files in which you implemented your solution (`src/lib.rs` in most cases). If you are using any external crates, please consider submitting the `Cargo.toml` file. This will make the review process faster and clearer. + +## Feedback, Issues, Pull Requests + +The [exercism/rust](https://github.com/exercism/rust) repository on GitHub is the home for all of the Rust exercises. If you have feedback about an exercise, or want to help implement new exercises, head over there and create an issue. Members of the rust track team are happy to help! + +If you want to know more about Exercism, take a look at the [contribution guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md). + +[help-page]: https://exercism.io/tracks/rust/learning +[modules]: https://doc.rust-lang.org/book/ch07-02-modules-and-use-to-control-scope-and-privacy.html +[cargo]: https://doc.rust-lang.org/book/ch14-00-more-about-cargo.html +[rust-tests]: https://doc.rust-lang.org/book/ch11-02-running-tests.html + +## Source + +Peter Goodspeed-Niklaus [https://github.com/coriolinus/fizzy](https://github.com/coriolinus/fizzy) + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/fizzy/example.rs b/exercises/fizzy/example.rs new file mode 100644 index 000000000..090f8d359 --- /dev/null +++ b/exercises/fizzy/example.rs @@ -0,0 +1,169 @@ +use std::ops::Rem; + +pub type MatchFn = Box bool>; + +pub struct Matcher { + matcher: MatchFn, + subs: String, +} + +impl Matcher { + pub fn new(matcher: F, subs: S) -> Matcher + where + F: Fn(T) -> bool + 'static, + S: AsRef, + { + Matcher { + matcher: Box::new(matcher), + subs: subs.as_ref().to_string(), + } + } +} + +#[derive(Default)] +pub struct Fizzy(Vec>); + +impl Fizzy +where + T: Copy + ToString, +{ + pub fn new() -> Self { + Fizzy(Vec::new()) + } + + pub fn add_matcher(mut self, matcher: Matcher) -> Self { + let Fizzy(ref mut matchers) = self; + matchers.push(matcher); + self + } + + pub fn apply_to(&self, item: T) -> String { + let Fizzy(ref matchers) = self; + let mut out = String::new(); + for matcher in matchers { + if (matcher.matcher)(item) { + out += &matcher.subs; + } + } + if out.is_empty() { + out = item.to_string() + } + out + } + + /// convenience function: equivalent to `iter.map(move |item| self.apply_to(item))`. + pub fn apply(self, iter: I) -> impl Iterator + where + I: Iterator, + { + iter.map(move |item| self.apply_to(item)) + } +} + +impl From>> for Fizzy { + fn from(matchers: Vec>) -> Fizzy { + Fizzy(matchers) + } +} + +pub fn fizz_buzz() -> Fizzy +where + T: Copy + Default + From + PartialEq + Rem + 'static, +{ + let three: T = 3.into(); + let five: T = 5.into(); + + Fizzy(vec![ + Matcher::new(move |n| n % three == T::default(), "fizz"), + Matcher::new(move |n| n % five == T::default(), "buzz"), + ]) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_fizz_buzz() { + let expect = vec![ + "1", "2", "fizz", "4", "buzz", "fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", + "14", "fizzbuzz", "16", + ]; + let got = fizz_buzz().apply(1..=16).collect::>(); + assert_eq!(expect, got); + } + + #[test] + fn test_fizz_buzz_u8() { + let expect = vec![ + "1", "2", "fizz", "4", "buzz", "fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", + "14", "fizzbuzz", "16", + ]; + let got = fizz_buzz().apply(1_u8..=16).collect::>(); + assert_eq!(expect, got); + } + + #[test] + fn test_fizz_buzz_u64() { + let expect = vec![ + "1", "2", "fizz", "4", "buzz", "fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", + "14", "fizzbuzz", "16", + ]; + let got = fizz_buzz().apply(1_u64..=16).collect::>(); + assert_eq!(expect, got); + } + + #[test] + fn test_fizz_buzz_nonsequential() { + let collatz_12 = &[12, 6, 3, 10, 5, 16, 8, 4, 2, 1]; + let expect = vec![ + "fizz", "fizz", "fizz", "buzz", "buzz", "16", "8", "4", "2", "1", + ]; + let got = fizz_buzz() + .apply(collatz_12.into_iter().cloned()) + .collect::>(); + assert_eq!(expect, got); + } + + #[test] + fn test_fizz_buzz_custom() { + let expect = vec![ + "1", "2", "Fizz", "4", "Buzz", "Fizz", "Bam", "8", "Fizz", "Buzz", "11", "Fizz", "13", + "Bam", "BuzzFizz", "16", + ]; + let fizzer = Fizzy::new() + .add_matcher(Matcher::new(|n| n % 5 == 0, "Buzz")) + .add_matcher(Matcher::new(|n| n % 3 == 0, "Fizz")) + .add_matcher(Matcher::new(|n| n % 7 == 0, "Bam")) + .apply(1..=16); + let got = fizzer.collect::>(); + assert_eq!(expect, got); + } + + #[test] + fn test_map() { + let expect = vec![ + "1", "2", "fizz", "4", "buzz", "fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", + "14", "fizzbuzz", "16", + ]; + let fb = fizz_buzz(); + let got = (1..=16) + .map(move |item| fb.apply_to(item)) + .collect::>(); + assert_eq!(expect, got); + } + + #[test] + fn test_fizz_buzz_f64() { + let expect = vec![ + "1", "2", "fizz", "4", "buzz", "fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", + "14", "fizzbuzz", "16", + ]; + // a tiny bit more complicated becuase range isn't natively implemented on floats + let got = fizz_buzz() + .apply(std::iter::successors(Some(1.0), |prev| Some(prev + 1.0))) + .take(16) + .collect::>(); + assert_eq!(expect, got); + } +} diff --git a/exercises/fizzy/src/lib.rs b/exercises/fizzy/src/lib.rs new file mode 100644 index 000000000..796494fbe --- /dev/null +++ b/exercises/fizzy/src/lib.rs @@ -0,0 +1,46 @@ +// the PhantomData instances in this file are just to stop compiler complaints +// about missing generics; feel free to remove them + +/// A Matcher is a single rule of fizzbuzz: given a function on T, should +/// a word be substituted in? If yes, which word? +pub struct Matcher(std::marker::PhantomData); + +impl Matcher { + pub fn new(_matcher: F, _subs: S) -> Matcher { + unimplemented!() + } +} + +/// A Fizzy is a set of matchers, which may be applied to an iterator. +/// +/// Strictly speaking, it's usually more idiomatic to use `iter.map()` than to +/// consume an iterator with an `apply` method. Given a Fizzy instance, it's +/// pretty straightforward to construct a closure which applies it to all +/// elements of the iterator. However, we're using the `apply` pattern +/// here because it's a simpler interface for students to implement. +/// +/// Also, it's a good excuse to try out using impl trait. +pub struct Fizzy(std::marker::PhantomData); + +impl Fizzy { + pub fn new() -> Self { + unimplemented!() + } + + // feel free to change the signature to `mut self` if you like + pub fn add_matcher(self, _matcher: Matcher) -> Self { + unimplemented!() + } + + /// map this fizzy onto every element of an interator, returning a new iterator + pub fn apply(self, _iter: I) -> impl Iterator { + // unimplemented!() doesn't actually work, here; () is not an Iterator + // that said, this is probably not the actual implementation you desire + Vec::new().into_iter() + } +} + +/// convenience function: return a Fizzy which applies the standard fizz-buzz rules +pub fn fizz_buzz() -> Fizzy { + unimplemented!() +} diff --git a/exercises/fizzy/tests/fizzy.rs b/exercises/fizzy/tests/fizzy.rs new file mode 100644 index 000000000..af8a07c6a --- /dev/null +++ b/exercises/fizzy/tests/fizzy.rs @@ -0,0 +1,125 @@ +use fizzy::*; + +macro_rules! expect { + () => { + vec![ + "1", "2", "fizz", "4", "buzz", "fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", + "14", "fizzbuzz", "16", + ] + }; +} + +#[test] +fn test_simple() { + let got = fizz_buzz().apply(1..=16).collect::>(); + assert_eq!(expect!(), got); +} + +#[test] +#[ignore] +fn test_u8() { + let got = fizz_buzz().apply(1_u8..=16).collect::>(); + assert_eq!(expect!(), got); +} + +#[test] +#[ignore] +fn test_u64() { + let got = fizz_buzz().apply(1_u64..=16).collect::>(); + assert_eq!(expect!(), got); +} + +#[test] +#[ignore] +fn test_nonsequential() { + let collatz_12 = &[12, 6, 3, 10, 5, 16, 8, 4, 2, 1]; + let expect = vec![ + "fizz", "fizz", "fizz", "buzz", "buzz", "16", "8", "4", "2", "1", + ]; + let got = fizz_buzz() + .apply(collatz_12.into_iter().cloned()) + .collect::>(); + assert_eq!(expect, got); +} + +#[test] +#[ignore] +fn test_custom() { + let expect = vec![ + "1", "2", "Fizz", "4", "Buzz", "Fizz", "Bam", "8", "Fizz", "Buzz", "11", "Fizz", "13", + "Bam", "BuzzFizz", "16", + ]; + let fizzer = Fizzy::new() + .add_matcher(Matcher::new(|n| n % 5 == 0, "Buzz")) + .add_matcher(Matcher::new(|n| n % 3 == 0, "Fizz")) + .add_matcher(Matcher::new(|n| n % 7 == 0, "Bam")) + .apply(1..=16); + let got = fizzer.collect::>(); + assert_eq!(expect, got); +} + +#[test] +#[ignore] +fn test_f64() { + // a tiny bit more complicated becuase range isn't natively implemented on floats + // NOTE: this test depends on a language feature introduced in Rust 1.34. If you + // have an older compiler, upgrade. If you have an older compiler and cannot upgrade, + // feel free to ignore this test. + let got = fizz_buzz() + .apply(std::iter::successors(Some(1.0), |prev| Some(prev + 1.0))) + .take(16) + .collect::>(); + assert_eq!(expect!(), got); +} + +#[test] +#[ignore] +fn test_minimal_generic_bounds() { + // NOTE: this test depends on a language feature introduced in Rust 1.34. If you + // have an older compiler, upgrade. If you have an older compiler and cannot upgrade, + // feel free to ignore this test. + use std::fmt; + use std::ops::{Add, Rem}; + + #[derive(Clone, Copy, Debug, Default, PartialEq)] + struct Fizzable(u8); + + impl From for Fizzable { + fn from(i: u8) -> Fizzable { + Fizzable(i) + } + } + + impl fmt::Display for Fizzable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let Fizzable(ref n) = self; + write!(f, "{}", n) + } + } + + impl Add for Fizzable { + type Output = Fizzable; + fn add(self, rhs: Fizzable) -> Fizzable { + let Fizzable(n1) = self; + let Fizzable(n2) = rhs; + Fizzable(n1 + n2) + } + } + + impl Rem for Fizzable { + type Output = Fizzable; + fn rem(self, rhs: Fizzable) -> Fizzable { + let Fizzable(n1) = self; + let Fizzable(n2) = rhs; + Fizzable(n1 % n2) + } + } + + let got = fizz_buzz() + .apply(std::iter::successors(Some(Fizzable(1)), |prev| { + Some(*prev + 1.into()) + })) + .take(16) + .collect::>(); + assert_eq!(expect!(), got); +} From 2058e5126b1f5d47a4a26aff4dcc499313e0d7b0 Mon Sep 17 00:00:00 2001 From: Peter Goodspeed-Niklaus Date: Fri, 3 May 2019 23:45:31 +0200 Subject: [PATCH 2/3] ensure stubs compile by giving tests more type hints --- exercises/fizzy/tests/fizzy.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/exercises/fizzy/tests/fizzy.rs b/exercises/fizzy/tests/fizzy.rs index af8a07c6a..29c7a84bb 100644 --- a/exercises/fizzy/tests/fizzy.rs +++ b/exercises/fizzy/tests/fizzy.rs @@ -11,21 +11,21 @@ macro_rules! expect { #[test] fn test_simple() { - let got = fizz_buzz().apply(1..=16).collect::>(); + let got = fizz_buzz::().apply(1..=16).collect::>(); assert_eq!(expect!(), got); } #[test] #[ignore] fn test_u8() { - let got = fizz_buzz().apply(1_u8..=16).collect::>(); + let got = fizz_buzz::().apply(1_u8..=16).collect::>(); assert_eq!(expect!(), got); } #[test] #[ignore] fn test_u64() { - let got = fizz_buzz().apply(1_u64..=16).collect::>(); + let got = fizz_buzz::().apply(1_u64..=16).collect::>(); assert_eq!(expect!(), got); } @@ -36,7 +36,7 @@ fn test_nonsequential() { let expect = vec![ "fizz", "fizz", "fizz", "buzz", "buzz", "16", "8", "4", "2", "1", ]; - let got = fizz_buzz() + let got = fizz_buzz::() .apply(collatz_12.into_iter().cloned()) .collect::>(); assert_eq!(expect, got); @@ -49,12 +49,11 @@ fn test_custom() { "1", "2", "Fizz", "4", "Buzz", "Fizz", "Bam", "8", "Fizz", "Buzz", "11", "Fizz", "13", "Bam", "BuzzFizz", "16", ]; - let fizzer = Fizzy::new() - .add_matcher(Matcher::new(|n| n % 5 == 0, "Buzz")) - .add_matcher(Matcher::new(|n| n % 3 == 0, "Fizz")) - .add_matcher(Matcher::new(|n| n % 7 == 0, "Bam")) - .apply(1..=16); - let got = fizzer.collect::>(); + let fizzer: Fizzy = Fizzy::new() + .add_matcher(Matcher::new(|n: i32| n % 5 == 0, "Buzz")) + .add_matcher(Matcher::new(|n: i32| n % 3 == 0, "Fizz")) + .add_matcher(Matcher::new(|n: i32| n % 7 == 0, "Bam")); + let got = fizzer.apply(1..=16).collect::>(); assert_eq!(expect, got); } @@ -65,7 +64,7 @@ fn test_f64() { // NOTE: this test depends on a language feature introduced in Rust 1.34. If you // have an older compiler, upgrade. If you have an older compiler and cannot upgrade, // feel free to ignore this test. - let got = fizz_buzz() + let got = fizz_buzz::() .apply(std::iter::successors(Some(1.0), |prev| Some(prev + 1.0))) .take(16) .collect::>(); @@ -115,7 +114,7 @@ fn test_minimal_generic_bounds() { } } - let got = fizz_buzz() + let got = fizz_buzz::() .apply(std::iter::successors(Some(Fizzable(1)), |prev| { Some(*prev + 1.into()) })) From e40e5316891780e402a70fa64d9b5859ec379d37 Mon Sep 17 00:00:00 2001 From: Peter Goodspeed-Niklaus Date: Wed, 8 May 2019 08:00:53 +0200 Subject: [PATCH 3/3] respond to reviewer feedback --- exercises/fizzy/.gitignore | 9 +++++++-- exercises/fizzy/example.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/exercises/fizzy/.gitignore b/exercises/fizzy/.gitignore index 2f88dbac5..db7f315c0 100644 --- a/exercises/fizzy/.gitignore +++ b/exercises/fizzy/.gitignore @@ -1,3 +1,8 @@ -/target +# Generated by Cargo +# will have compiled files and executables +/target/ **/*.rs.bk -Cargo.lock \ No newline at end of file + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/exercises/fizzy/example.rs b/exercises/fizzy/example.rs index 090f8d359..b166eac5e 100644 --- a/exercises/fizzy/example.rs +++ b/exercises/fizzy/example.rs @@ -159,7 +159,7 @@ mod test { "1", "2", "fizz", "4", "buzz", "fizz", "7", "8", "fizz", "buzz", "11", "fizz", "13", "14", "fizzbuzz", "16", ]; - // a tiny bit more complicated becuase range isn't natively implemented on floats + // a tiny bit more complicated because range isn't natively implemented on floats let got = fizz_buzz() .apply(std::iter::successors(Some(1.0), |prev| Some(prev + 1.0))) .take(16)