コンテンツへスキップ

FizzBuzzを『構造』で解く:Rustのイテレータとマクロで遊ぶ深淵

fizz buzzで遊ぼう

fizz buzzはプログラム書く時の定番のお遊びなのでよくみかけます。ただ作るだけだったらおもろないので、深堀りしてみましょう。シンプルなfor/if分岐を使った例は検索すればすぐに見つかると思うのであえて書きません。

イテレータでやってみよう

早速ソースを見せましょ

fn main() {
    let fizz = ["", "", "Fizz"].iter().cycle();
    let buzz = ["", "", "", "", "Buzz"].iter().cycle();

    let pattern = fizz.zip(buzz).map(|(f, b)| format!("{}{}", f, b));

    pattern.enumerate().take(15).for_each(|(i,fb)| println!("{} {}, ",i+1, fb));
}Code language: JavaScript (javascript)

これでif分岐もfor分も使わずにfizzbuzzが作れる。ちょっと解説しておくとcycleアダプタというのは繰り返すパターンを作るもの。zipアダプタで2つをタプルにしてまとめて、format!をつかって合体させております。
最後にenumurateで順番を割り振ってprintln!で表示してます。enumurateは0からナンバリングして(i, (fizz,buzz))という形のデータにしてパイプラインに乗せて最後に先頭から15個データを取り出してfor_eachでループ処理(forループを使わないけど、実はtake+for_eachがforループの役目かな。

これで

$ cargo run --bin fizzbuzz
   Compiling sandbox v0.1.0 (/home/yasuto/projects/rust/sandbox)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running `/home/yasuto/projects/rust/sandbox/target/debug/fizzbuzz`
1 ,
2 ,
3 Fizz,
4 ,
5 Buzz,
6 Fizz,
7 ,
8 ,
9 Fizz,
10 Buzz,
11 ,
12 Fizz,
13 ,
14 ,
15 FizzBuzz,Code language: JavaScript (javascript)

と表示されます。ここではfizzbuzz.rsとしてファイルを作成しています。cargo init sandboxとして、 プロジェクト/src/bin/の下に作ってます。

これで単純なイテレータで使うやり方ですね。次行きましょう。

抽象化してみよう

ここから難易度が上がります。Option型を使ってmapを使える形にしてみましょう。Optionにすることでパイプラインを構築する時の基本をおえる訓練です。データとしてスッキリ扱える利点があるのがOption型なんです。

fn checked_word(word: &str, n: usize, i: usize) -> Option<String> {
    if i % n == 0 { Some(word.to_string()) } else { None }
}Code language: JavaScript (javascript)

どうしてこうしてるかと言うと、適当な言葉で周期的にSome(word),Noneの配列を作りたいからなんですね。こうすることでイテレータで扱いやすくなる形なのです。
単純に checked_word("fizz".to_string(),3)とすればfizzのエンジンになるし、checked_word("buzz",5)とすればbuzzのエンジンにも化けます。これが抽象化のキモです。
これでどうかけるかというと、

let fizzes = (1..=3).map(|i| checked_word("Fizz", 3, i)).collect::<Vec<_>>().into_iter().cycle();
let buzzes = (1..=5).map(|i| checked_word("Buzz", 5, i)).collect::<Vec<_>>().into_iter().cycle();

let pattern = fizzes.zip(buzzes);Code language: JavaScript (javascript)

とすれば fizz, buzzのイテレータが完了です。無限にfizz, buzz配列(無限ストリーム)ができるんですね。そしてpatternでzipでタプルにしてる。それを完成形にすると次のようになります。

fn checked_word(word: &str, n: usize, i: usize) -> Option<String> {
    if i % n == 0 { Some(word.to_string()) } else { None }
}

fn main() {
    let fizzes = (1..=3).map(|i| checked_word("Fizz", 3, i)).collect::<Vec<_>>().into_iter().cycle();
    let buzzes = (1..=5).map(|i| checked_word("Buzz", 5, i)).collect::<Vec<_>>().into_iter().cycle();

    let pattern = fizzes.zip(buzzes);

    pattern
        .enumerate()
        .map(|(i, (f, b))| {
            let res = format!("{} {}{},",
                i + 1,
                f.unwrap_or_default(),
                b.unwrap_or_default(),
            );
            if res.is_empty() { (i + 1).to_string() } else { res }
        })
        .take(15)
        .for_each(|line| println!("{}", line));
}Code language: PHP (php)

unwrap_or_defaultってのはSome(“fizz”)なら”fizz”とかえして、Noneなら””を返す仕組みですね。これで同じ出力結果に変わります。

さらなる抽象化をしてみる その1ヘルパー関数にする

fizzes, buzzedは同じ形ですね。これは関数にまとめてみようとなります。そこで次のように化けます。

fn create_cycle_word(word: &'static str, n: usize) -> impl Iterator<Item = Option<String>> {
    (1..=n)
        .map(move |i| checked_word(word, n, i))
        .collect::<Vec<_>>()
        .into_iter()
        .cycle()
}

// let fizzes = create_cycle_word("fizz", 3); という使い方

こういうヘルパー関数を作るのですが、とたんに難易度が高く記述がややこしくなります。一つはライフタイムの扱いです。’static とmoveが登場することに気が付かれるとおもいます。これはヘルパー関数を作ることで所有権が移動するためにこの関数の利用の間利用する変数wordが生き残ってる保証をコンパイラが求めるためです。混乱の元ですね。このややこしさがrustの安全性に直結したことになってるので安全性と理解しやすさがトレード・オフの関係になった結果ですね。

これではヘルパー関数なんてめんどくさいつくりたくない!となりますよね?

さらなる抽象化をしてみる その2宣言マクロを使う

じゃあrustは記述が長いうざいプログラムばかりになるのか?と言われるとさらなる逃げ道があるんですね。それは多くの人が避けたがるマクロです。

macro_rules! ccw {
    ($word:expr, $n:expr) => {
        (1..=$n)
            .map(|i| checked_word($word, $n, i))
            .collect::<Vec<_>>()
            .into_iter()
            .cycle()
    };
}
// let fizzed ccw!("fizz", 3); という使い方Code language: PHP (php)

実はこのマクロで、ヘルパー関数より手軽に作れて可読性同等になるんですね。しかもマクロというのはコンパイルのときにインライン展開するので、ヘルパー関数よりオーバーヘッドがかからないということがあります。よく言われるコンパイルが遅いということはあるけど、実行時間を高速にするための担保だと思えばいいです。もちろん、質の悪いマクロはデバッグまでややこしくなるから恐れられるのですが、作り方を間違えなければ幸せになるんですね。

ここではややこしくなるので3種類に増やしたときとかもありますが、実はzipアダプタの限界がありまして、スッキリしたコードになりません。抽象化でここまでできますけど、その場合は for (fizz,buzz,woof) in zip!(fizzed,buzzed,woofs) { … } というフォーループを使ったほうがスッキリすることは書いておきますね

最終更新: 2026.01.24