-
Notifications
You must be signed in to change notification settings - Fork 543
Add fizzy: an exercise teaching advanced generics and impl trait #828
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
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a Rust-specific topic, but I think it's appropriate here. |
||
| "iterators" | ||
| ] | ||
| }, | ||
| { | ||
| "slug": "roman-numerals", | ||
| "uuid": "498be645-734a-49b7-aba7-aae1e051e1f0", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Generated by Cargo | ||
| # will have compiled files and executables | ||
| /target/ | ||
| **/*.rs.bk | ||
|
|
||
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| --- | ||
| blurb: "Implement FizzBuzz using advanced generics" | ||
| source: "Peter Goodspeed-Niklaus" | ||
| source_url: "https://github.com/coriolinus/fizzy" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| [package] | ||
| name = "fizzy" | ||
| version = "0.0.0" | ||
| edition = "2018" | ||
|
|
||
| [dependencies] | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| use std::ops::Rem; | ||
|
|
||
| pub type MatchFn<T> = Box<dyn Fn(T) -> bool>; | ||
|
|
||
| pub struct Matcher<T> { | ||
| matcher: MatchFn<T>, | ||
| subs: String, | ||
| } | ||
|
|
||
| impl<T> Matcher<T> { | ||
| pub fn new<F, S>(matcher: F, subs: S) -> Matcher<T> | ||
| where | ||
| F: Fn(T) -> bool + 'static, | ||
| S: AsRef<str>, | ||
| { | ||
| Matcher { | ||
| matcher: Box::new(matcher), | ||
| subs: subs.as_ref().to_string(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Default)] | ||
| pub struct Fizzy<T>(Vec<Matcher<T>>); | ||
|
|
||
| impl<T> Fizzy<T> | ||
| where | ||
| T: Copy + ToString, | ||
| { | ||
| pub fn new() -> Self { | ||
| Fizzy(Vec::new()) | ||
| } | ||
|
|
||
| pub fn add_matcher(mut self, matcher: Matcher<T>) -> 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<I>(self, iter: I) -> impl Iterator<Item = String> | ||
| where | ||
| I: Iterator<Item = T>, | ||
| { | ||
| iter.map(move |item| self.apply_to(item)) | ||
| } | ||
| } | ||
|
|
||
| impl<T> From<Vec<Matcher<T>>> for Fizzy<T> { | ||
| fn from(matchers: Vec<Matcher<T>>) -> Fizzy<T> { | ||
| Fizzy(matchers) | ||
| } | ||
| } | ||
|
|
||
| pub fn fizz_buzz<T>() -> Fizzy<T> | ||
| where | ||
| T: Copy + Default + From<u8> + PartialEq + Rem<Output = T> + '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::<Vec<_>>(); | ||
| 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::<Vec<_>>(); | ||
| 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::<Vec<_>>(); | ||
| 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::<Vec<_>>(); | ||
| 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::<Vec<_>>(); | ||
| 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::<Vec<_>>(); | ||
| 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 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) | ||
| .collect::<Vec<_>>(); | ||
| assert_eq!(expect, got); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<T>(std::marker::PhantomData<T>); | ||
|
|
||
| impl<T> Matcher<T> { | ||
| pub fn new<F, S>(_matcher: F, _subs: S) -> Matcher<T> { | ||
| 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<T>(std::marker::PhantomData<T>); | ||
|
|
||
| impl<T> Fizzy<T> { | ||
| pub fn new() -> Self { | ||
| unimplemented!() | ||
| } | ||
|
|
||
| // feel free to change the signature to `mut self` if you like | ||
| pub fn add_matcher(self, _matcher: Matcher<T>) -> Self { | ||
| unimplemented!() | ||
| } | ||
|
|
||
| /// map this fizzy onto every element of an interator, returning a new iterator | ||
| pub fn apply<I>(self, _iter: I) -> impl Iterator<Item = String> { | ||
| // 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<T>() -> Fizzy<T> { | ||
| unimplemented!() | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note on positioning: the Rust track currently has three exercises other than
fizzywith the "generics" topic. None of them are core exercises. The easiest of these is a difficulty 4, unlocked by "luhn", so I chose to make this a peer exercise.I don't believe that it's worth worrying too much about positioning, because Rust is currently in the middle of the track reordering project, and we can expect the position of this exercise to shift, possibly substantially.