Я редактирую свой репозиторий 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()))
    )
}

Это становится довольно абстрактным, но суть остается прежней: мы проверяем целое число для всех условий включения String. Если String не включен, мы просто возвращаем 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! Конечно, функция apply более сложная, чем наше закрытие, но я предполагал поместить ее в какую-то библиотеку. К сожалению, сейчас это невозможно сделать, потому что текущая заявка крайне ограничена моим конкретным случаем. Видеть:

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 ›быстрее, чем String!), Документацию, зависимости Monoid и т. Д. Я столкнулся с ограничениями функционального программирования в Rust, которые сделали невозможным обобщение функции apply. Я очень рад возможности скомпилировать его на стабильном Rust без необходимости изменять код, чтобы он выглядел более некрасиво. Я с нетерпением жду дальнейших улучшений в Rust, которые позволят мне убрать разыменование из моей функции apply и иметь возможность вставлять apply в какую-то внешнюю библиотеку для всеобщего использования.

Окончательный результат здесь https://bitbucket.org/iopq/fizzbuzz-in-rust/src/

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