Rust言語でFizz Buzz問題を解く

『IT自動化の力でビジネス加速を全ての企業に』

”IT自動化の専門会社”、リアルグローブ・オートメーティッド(RGA)の技術ブログ編集部の馬塚です。本日もRGAの技師がまとめた技術情報を読者の皆様にお届けしていきます!

RGAではお客様が抱えている問題に対して、技師が持つ高い技術力と柔軟な思考力をもって、満足していただけるソリューションを提案し提供しているのですが、お客様のご要望次第で新しいプログラミング言語を短時間で学習し、ソリューション構築に使用する、なんてこともあったりします。

今回はそんなRGAの技師が初めて Rust 言語に触れて、実際に簡単なプログラムを書く様子と、そこで発見した Rust 言語の特徴などを、特に Rust にちょっと興味あるけどまだ手を出していない方々に向けて紹介してみたいと思います。

――――――――――――――――――――――――――――――――――

経緯

最近よく目にするようになってきたプログラミング言語に、Rust言語がある。Rust は Mozilla が支援して開発が進められている比較的新しい言語で、速度、並行性、安全性を言語仕様として保証することが大きな特徴。C/C++に代わるシステムプログラミングに適した言語、と言われることもあり、何とも面白そうではないですか。

ちょうど社内で、プログラミング力を測るオンラインテストのようなものを開発しており、その中で有名な Fizz Buzz 問題をちょっとだけ難しくした課題を任意の言語で作成する、というのがあった。それを知った同僚が、自身の勉強を兼ねて Go で答えを書いたのを見て、それならば、と少し前から気になっていた Rust 言語を調べながら使って回答してみようかな、と思い立ってしまった。この時点で Rust 言語を使う案件があったわけでないが、一度こう思ってしまったらもう誰も止められない……。

解く問題

問題の内容は以下のようなもの。

正整数 N,M(N<M) と素数 p,q(p<q) が与えられます。このとき、N≤x≤Mな整数 x について、以下の条件に従った出力します。
1. p の倍数ならば Fizz と出力する。
2. q の倍数ならば Buzz と出力する。
3. x が素数ならば Prime と出力する。
4. 上記条件を複数満たすならば、条件 1 ~ 3 の順に満たした条件の出力を行う。ただし、間に半角スペースを 1 つ加える。 (例: Fizz Prime)
5. どの条件にも当てはまらないならば、その数字 x を出力する。

ここで、以下のような制約がある。

・1≤N≤100
・2≤M≤100
・N<M
・2≤p≤50
・2≤q≤50
・p<q

入力と出力の例は以下のようになる。

入力例:

N = 5
M = 15
p = 3
q = 5

出力例:

Buzz Prime
Fizz
Prime
8
Fizz
Buzz
Prime
Fizz
Prime
14
Fizz Buzz

回答戦略

Rust 言語を使うのも初めてなので、うまくいくかどうかわからないが、以下のような考えで書き始めてみることにした。

・素数判定はライブラリにやってもらおう(後で判定機は実装する)
・"p,qの倍数である判定" と "x が素数かの判定" をそれぞれで行ったほうが、最後の出力的には楽なのでは?
・”複数条件に当てはまったときに半角スペースを入れて出力” が厄介なので、サボりたいお気持ち
・match を使ったらうまい感じに書けそうな気がするので試してみよう

書いてみた

実際に書いてみたコードは下のようなもの。Rust を1から勉強しながらおよそ4時間くらいで、とりあえず問題の回答に到達できた。

extern crate primal;

use std::io;

fn read_nums_pair() -> (u64, u64) {
   let s = {
       let mut s = String::new();
       io::stdin().read_line(&mut s).unwrap();
       s.trim_end().to_owned()
   };

   let mut ws = s.split_whitespace();
   let n: u64 = ws.next().unwrap().parse().unwrap();
   let m: u64 = ws.next().unwrap().parse().unwrap();
   (n, m)
}

fn main() {
   let (start, end) = read_nums_pair();
   let (prime_1, prime_2) = read_nums_pair();

   for i in start..(end + 1) {
       let fizzbuzz: &str =
               match (i % prime_1, i % prime_2) {
                   (0, 0) => "Fizz Buzz",
                   (0, _) => "Fizz",
                   (_, 0) => "Buzz",
                   _ => "",
               };

       let is_prime: bool  = primal::is_prime(i);


       match (fizzbuzz.len(), is_prime) {
           (0, false) => println!("{}", i.to_string()),
           (_, false) => println!("{}", fizzbuzz),
           (0, true) => println!("Prime"),
           _ => println!("{}", format!("{} Prime", fizzbuzz.to_string()))
       }
   }
}

match についての簡単な説明

このコードをしばらく眺めていれば、Rust の特徴的な書式がいろいろ見えてくるだろう。要の部分は何と言っても match 式を使っている部分である。match 式自体は Rust 特有のものではないが、馴染み深くはないだろうと思われるので、簡単な説明を行っておこう。

match 式では評価したい値に対して一連のパターンを上から順に比較し、マッチしたパターンに応じてコードを実行してくれる。簡単な例として、少々雑だが整数 x (255 >= x >= 0) を文字列へと変換して標準出力をする関数を作ってみる。入力する x が x = 1, 3, 5, 7 であれば対応する英単語に変換してくれるというものを考えてみよう。

fn numberToString (x: u8) {
   match x {
       1 => println!("one"),
       3 => println!("three"),
       5 => println!("five"),
       7 => println!("seven"),
       _ => (),
   }
}

このとき x = 1 を入れれば、最初のパターンとマッチし terminal 上に “one” と出力される。x = 3 であれば、最初のパターンは当然マッチしないので、次のパターンと比較される。予想できるだろうが、今回は terminal 上に “three” と表示されるだろう。

match 式で気をつけなければいけないことは、「あり得る全ての可能性を網羅しておかなければならない」ということだ。上記では x = 1, 3, 5, 7 のときの英単語はパターンとして準備されているが、実際には x = 2, 4, 6, … と英単語が準備されていない数字が入りうる。ここで特別なパターン `_` を利用することで、この問題を回避することが可能だ。 特別なパターン `_` はどんな値にもマッチするようになっている。つまり、上記例の match 式ででは `_` は「x = 1, 3, 5, 7 のパターンとマッチしなかった値」の全てとマッチするようになっている。これによって、あり得る全ての可能性は網羅できたわけだ。

今回のコードでは match 式を利用し、パターン分けを比較的簡素に書くことで見通しを良くしてみようという取り組みを行ってみた。倍数判定は「余りが0か」というシンプルな条件で見たいものの、if 式でふるいにかけようとすると少し長ったらしくなってしまう。そこで match 式でまとめてみたという感じで書いてみた。

書いてみて気づいたこと

以下は問題を解きながら Rust 言語を数時間触ってみての雑な感想。

回答する中でこだわりたかったところ

・match をうまいこと使えば if 文を無闇に使わないで綺麗に書けそう、という予想は的中し、いい感じにRustのコードっぽくなった
・最後の println! の場合分けをスッキリ書きたかったが断念した

Rust の好きなところ

・match を使えば、簡単に条件分けできて嬉しいので好き
・条件と行いたい処理がごちゃごちゃと続かないので好き
・変数がデフォルトでイミュータブルなので好き
・必要なところだけ使い回しすればいいので、あとで間違えた代入をすることが減りそうで好き(頭のメモリを注意に割かなくても、割となんとかなる)
・VScode と rls の組み合わせで書いてみたけど、いろいろ教えてくれるからいい(型でのトラブルもすぐわかる)
・そんなに苦労せずにいい感じに環境が作れた

困ったところ

・標準入力の実装が思ったより面倒で、いい感じのライブラリが欲しくなる(たぶんあるんだろうけど)
・&str と String の違いが分かりにくかった
・; の有無で挙動が変わるのは注意しないと...

まとめ

以前から「Rust はC++並に速い。型の安全性も強く、メモリ管理など煩わしさがない。」といった感じでオススメされていた。今回はその Rust の強みを最大限に活かすには、規模がいささか小さいとは思われるが、確かに片鱗を味わえた気がする。

ちょっとした処理を行う関数でも、しっかりと型を考えていかないとコンパイラや linter にエラーを吐かれ、頭をひねりながら「何が駄目なんだろう」と悩む時間はたしかに多かった。しかし、この「少しずつ考える時間」のおかげで自分の適当に考えていた点はあぶり出され、甘い考えをミッチリと矯正されたような感覚だった。興味が湧いて、なんとなく手を出したが Rust を触ったことはいい経験になった。

これまでは静的型付け言語をしっかりと触る機会は少なく、せいぜい TypeScript 程度だったが Rust は処理をしたい変数の型をしっかりと意識し、基本的にはイミュータブルという点で個人的には気に入った。規模が大きいプロジェクトのコードを触る機会は過去にあったが、よく型でトラブルを起こした記憶があったため、この安全性は非常に嬉しいものだ。また、今回の規模程度では大きな差が出ないと思われるものの、速度が非常に速いことは嬉しいものだ。将来のトラブルを少しずつ未然に防ぎ、それでいて高速な処理が行えることは願ってもないことであり。

この記事では初心者が Rust を書いてみたということで真骨頂と言える部分は大きく触れられてはいないだろう。しかし、それでも Rust を触ったことで面白いなと思える経験はたしかにあった。環境のセットアップも非常に簡単で terminal にコピーアンドペーストの1行で完了してしまう。初めての静的型付け言語に…というには少しハードルは高い気もするが、ぜひ一度触ってみてほしい言語である。

――――――――――――――――――――――――――――――――――

執筆者プロフィール:合屋 純
九州大学大学院理学府修士卒 大学院で物理学を専攻し、数値計算プログラムに触れたことをキッカケにエンジニアリングに興味を持つ。
現在はリアルグローブ・オートメーティッドにて、コンテナ関連技術や自動化ツールの導入業務に従事。

【ご案内】
ITシステム開発やITインフラ運用の効率化、高速化、品質向上、その他、情シス部門の働き方改革など、IT自動化導入がもたらすメリットは様々ございます。
IT業務の自動化にご興味・ご関心ございましたら、まずは一度、IT自動化の専門家リアルグローブ・オートメーティッド(RGA)にご相談ください!

お問合せは以下の窓口までお願いいたします。

【お問い合わせ窓口】
株式会社リアルグローブ・オートメーティッド
代表窓口:info@rg-automated.jp
URL:https://rg-automated.jp

画像1