Редактирам моето хранилище на FizzBuzz от 2014 г. След четири години най-накрая успях да превключа от нощен към стабилен поради версията 1.26. Нека направим малко резервно копие и да оценим промените след първата ревизия.

trait Monoid {
    // don't have assoc. values yet, so us a nullary function
    fn id() -> Self;
    // an associative binary operation
    // this version consumes arguments
    // a non-consuming version might be possible
    fn op(self, other: Self) -> Self;
}

// owned strings implement append
impl Monoid for ~str {
    fn id() -> ~str { ~"" } // identity is empty string
    fn op(self, other: ~str) -> ~str {
        self.append(other)
    }
}

// not sure if we can impl Monoid for &str

// Options are Monoids if they contain Monoids
impl<A: Monoid> Monoid for Option<A> {
    fn id() -> Option<A> { None }
    fn op(self, other: Option<A>) -> Option<A> {
        match (self, other) {
             (None, b) => b,
             (a, None) => a,
             (Some(a), Some(b)) => Some(a.op(b)),
        }
     }
 }

 fn fizzbuzz(i: int) -> ~str {
     // filtered is the equivalent a comprehension guard
     // unwrap_or is fromMaybe
     Some(~"fizz").filtered(|_| i % 3 == 0).op(
     Some(~"buzz").filtered(|_| i % 5 == 0)
     // we can add more conditions by appending 
     // a .op( above and inserting a new line below
     ).unwrap_or(i.to_str())
 }

 fn main() {
	let args = std::os::args();
	
	match from_str::<int>(args[1]){
         Some(x)=>for i in std::iter::range_inclusive(1, x) {
			  println!("{}", fizzbuzz(i));
			},
         None=>println!("I need a real number")
    }
 }

Това беше първата версия и всъщност не беше написана от мен. Попитах как да пренеса https://web.archive.org/web/20130511210903/http://dave.fayr.am/posts/2012-10-4-finding-fizzbuzz.html към Rust и някой от IRC помогна ми.

Историческа бележка: погледнете ~ указателите, std::iter::range_inclusive, тип int и т.н.

Тази версия е хубава с това, че добавянето на условие като „ако се дели на 7, напишете Bazz“ е доста лесно. Но определено можем да се справим по-добре. Вместо да се налага да променяме програмата, всъщност можем просто да накараме нашата функция FizzBuzz да вземе масив от низове и делители. Междувременно трябваше да надстроя до Rust 0.11 и да превключа от ~str към типа String.

fn fizzbuzz(i: int) -> String {
	// filtered is the equivalent a comprehension guard
	// unwrap_or is fromMaybe
	fizzbuzz_op(i, None, 
			[("Fizz".to_string(), 3), ("Buzz".to_string(), 5), ("Bazz".to_string(), 7)]
	).unwrap_or(i.to_str())
}

fn fizzbuzz_op(i: int, res: Option<String>, rest: &[(String, int)]) -> Option<String> {
    match rest {
	[ref first, .. tail] => fizzbuzz_op(
			i,
			res.op(Some(first.ref0().clone()).filtered(|_| i % *first.ref1() == 0)),
			tail
		),
    _ => res
	}
}

Това все още не е толкова общо. Защо условието винаги е „делимо на“, а не нещо друго?

pub fn op_filter(tuples: &[(&'static str, &Fn() -> bool)]) -> Option<String> {
    tuples.iter().fold(None, |res, &(value, include)|
            res.op(utils::filter(Some(value.to_string()), include()))
    )
}

Това става доста абстрактно, но смисълът е все същият: ние тестваме цяло число за всички условия за включване на низ. Ако низът не е включен, ние просто връщаме None. Проблемът с тази функция е, че обединява операцията по сгъване с филтрирането. Но преди да поправим това, range_inclusive беше премахнат, така че сега или трябва да напишем цикъл while и да повторим ръчно, или имаме грешка при препълване, когато потребителят ни даде максималния int. Това е първият стабилен блокер на FizzBuzz. Просто е грозно да правите цикъл while или да имате препълване!

Потребител на /r/rust предложи да направим това

for i in 1..n.checked_add(1).expect("Integer overflow")  { ... }

Но това обработва една стойност по-малко от правилния включващ диапазон, освен това е грозно. Другият блокер се появи, когато започнах да пиша филтърната си функция.

//does the monoid operation on the slice of tuples if the closure evaluates to true
fn accumulate<'a, T: Monoid>(tuples: &[(&'a str, &Fn(i32) -> bool)], i: i32) -> Option<T> 
        where &'a str: Into<T> {
        
    tuples.iter()
        .filter(|&x|second(x)(i)) //don't try to make this point-free it's point-less
        .map(first)
		.cloned()
		.map(<&str>::into)  
        .fold1(T::op)
        
        //op just concatenates, but Cow<'a, str> does not satisfy Add
}

Тук използваме fold1 от itertools. Той сгъва, започвайки от None и сгъва Option‹Monoid› с помощта на Monoid::op (което в нашия случай е конкатенация).

Въпреки този коментар, все пак исках да направя тази част от филтъра без точки, защото не ми харесва как е по-трудно да се чете с повече сигили.

//does the monoid operation on the slice of tuples if the closure evaluates to true
fn accumulate<'a, T: Monoid>(tuples: &[(&'a str, &Fn(i32) -> bool)], i: i32) -> T
        where T: From<&'a str> + From<String> {

    tuples.iter()
        .filter(apply(second, i))
        .map(first)
        .cloned()
        .map(<&str>::into)
        .fold1(T::op)
        .unwrap_or_else(|| i.to_string().into())
        //op just concatenates, but String does not satisfy Add
}

fn apply<A, B, C, F, G>(mut f: F, a: A) 
-> impl FnMut(&B) -> C // must still be `for<'r> impl FnMut(&'r B) -> C`, because that’s what filter requires
         where F: FnMut(B) -> G, // must not be `for<'r> FnMut(&'r B) -> G`, because regular functions do not implement it
               G: FnMut(A) -> C,
               B: Copy, // for dereferencing
               A: Clone {

    move |b| f(*b)(a.clone()) // this must do any bridging necessary to satisfy the requirements
}

Това е вторият ни блокировач. За да направя функция, която прилага аргумент и връща затваряне, вземайки друг аргумент, трябваше да използвам екзистенциални типове. Друга функция на 1.26! Разбира се, функцията за прилагане е по-сложна от нашето затваряне, но си представях да поставя това в някакъв вид библиотека. За съжаление, това не може да бъде направено сега, тъй като настоящото приложение е изключително ограничено до моя точен случай. Вижте:

https://stackoverflow.com/questions/39541312/function-returning-a-closure-not-working-inside-my-filter

По-обща версия вероятно е възможна, когато Rust има по-добър полиморфизъм от по-висок вид. Но с версията на 1.26 мога да компилирам тази функция в стабилна версия, защото поддържа -› impl Fn. С пускането на включващи диапазони мога да направя и нещо подобно:

    let acc = (1..=15).map(|i| fizzbuzz::fizzbuzz(&[
            ("Fizz", &|i: i32| i % 3 == 0),
            ("Buzz", &|i: i32| i % 5 == 0),
        ], i)).collect::<Vec<_>>().join(" ");
 
    assert_eq!(acc, "1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz".to_string());

През годините изучавах Rust и бях в крак с напредъка му, като модифицирах това хранилище. Написах тестове, бенчмаркове (Cow‹’a, str›is faster than String!), документация, Monoid зависимост и т.н. Сблъсках се с ограниченията на функционалното програмиране в Rust, които не направиха възможно обобщаването на функцията за прилагане. Много съм развълнуван от възможността да го компилирам на стабилен Rust, без да се налага да променям кода си, за да изглежда по-грозен. Очаквам с нетърпение по-нататъшни подобрения на Rust, които биха ми позволили да премахна дереферирането от моята функция за прилагане и да мога да залепя приложението в някаква външна библиотека за употреба от всички.

Крайният резултат е тук https://bitbucket.org/iopq/fizzbuzz-in-rust/src/

Допълнение: Оттогава актуализирах моя Monoid сандък, за да използвам const ID вместо функция. Версия 0.0.6 вече изисква нощната версия на Rust, тъй като String::new() не е const на стабилен.