The Rust Programming Language の章末問題を解く(3章)
はじめに
こんにちは、かべです。最近 Rust の勉強を続けているので、そこでの練習を少しずつこちらにもまとめていきます。今は The Rust Programming Language 日本語版 を読み進めているので、章末にまれにある練習問題を解いたものやサンプルのちょっとした改造を載せていきます。
今回のテーマは こちら の練習問題です。
環境
以下の環境で動作させています。
- Ubuntu 20.04.1 LTS
- rustc 1.61.0 (fe5b13d68 2022-05-18)
- cargo 1.61.0 (a028ae4 2022-04-29)
温度の変換
温度を摂氏と華氏で変換するプログラムです。ちなみに摂氏 x 度と華氏 y 度の関係はそれぞれ y = 1.8*x + 32
, x = (y-32)/1.8
です。値が欲しいだけならこの関係と元の温度をハードコーディングすれば良いですが、せっかくなので2章で作った数当てゲームで得た知識を使って書いていきます。
最終的な目標は次の通りです。
- 摂氏から華氏に変換するか、華氏から摂氏に変換するかを選べる
- 数値を入力すると、選んだ変換方向で計算が行われて結果が得られる
- 途中で不正な値が入力された時には、再度入力を求める
まずは cargo new --bin temperature
などでディレクトリを作成します。始めにどのような変換をするのかを入力から受け取るコードを書きます。これは数当てゲームのコードを少し改造して次のように書くことが出来ます。
loop { println!("To change from Celsius to Fahrenheit, press 1."); println!("To change from Fahrenheit to Celsius, press 2."); // Get input and convert to integer let mut command = String::new(); io::stdin() .read_line(&mut command) .expect("Failed to read line"); let command: u32 = match command.trim().parse() { Ok(num) => { // If input is invalid, let users try again if num != 1 && num != 2 { continue; } num }, Err(_) => continue, }; }
数当てゲームでは i32 として成立するものが入力された時は全て受け取っていましたが、今回は 1 または 2 が入力された時のみ受け取るように Ok
内を改造しています。
次は温度を入力として受け取り、先ほど入力した command
に従って変換を施して出力します。入力の受け取り方は上とほぼ同様で、温度の計算は今回 if 文を使って分岐させることにします。
loop { println!("Input the temperature."); // Get input and convert to float let mut temperature = String::new(); io::stdin() .read_line(&mut temperature) .expect("Failed to read line"); let temperature: f32 = match temperature.trim().parse() { Ok(num) => num, Err(_) => continue, }; // Calculate converted temperature if command == 1 { println!("{} C = {} F", temperature, temperature*1.8 + 32.0); } else { println!("{} F = {} C", temperature, (temperature - 32.0)/1.8); } break; }
最後に、温度を受け取るループは command
を受け取るループの内側になるのですが、計算した後に抜け出したいのは外側のループなので loop
にタグを付けてあげる作業が必要になります。これをすると、最終的なコードは以下のようになります。
use std::io; fn main() { 'outer: loop { println!("To change from Celsius to Fahrenheit, press 1."); println!("To change from Fahrenheit to Celsius, press 2."); // Get input and convert to integer let mut command = String::new(); io::stdin() .read_line(&mut command) .expect("Failed to read line"); let command: u32 = match command.trim().parse() { Ok(num) => { // If input is invalid, let users try again if num != 1 && num != 2 { continue; } num }, Err(_) => continue, }; loop { println!("Input the temperature."); // Get input and convert to float let mut temperature = String::new(); io::stdin() .read_line(&mut temperature) .expect("Failed to read line"); let temperature: f32 = match temperature.trim().parse() { Ok(num) => num, Err(_) => continue, }; // Calculate converted temperature if command == 1 { println!("{} C = {} F", temperature, temperature*1.8 + 32.0); } else { println!("{} F = {} C", temperature, (temperature - 32.0)/1.8); } break 'outer; } } }
cargo run
で実行してみると次のようになります。
To change from Celsius to Fahrenheit, press 1. To change from Fahrenheit to Celsius, press 2. 1 Input the temperature. 25 25 C = 77 F
フィボナッチ数列
おなじみフィボナッチ数列の n 項目を計算します。今回は関数を定義して実行してみます。cargo new --bin fibonacci
でディレクトリを作成します。
まずは定義に従って再帰関数を書いてみます。
fn fibonacci_recursive(n: u64) -> u64 { if n == 0 || n == 1 { n } else { fibonacci_recursive(n-1) + fibonacci_recursive(n-2) } }
この関数を n = 10
で実行すると 55
という正しい結果が得られます。ただこの実装は非常に遅く、例えば n = 50
などとするとプログラムがなかなか終了しません。これは再帰の際に同じ値を何度も計算していることに起因します。この問題を解決するには配列や HashMap などで一度計算した値を持っておき、再度その値が必要になった時には即座にそこから取り出すという実装をすることで回避できます。
他の解法として、今回は n の小さいものから計算してみます。
fn fibonacci_dp(n: u64) -> u64 { if n == 0 || n == 1 { return n; } let mut a = 0; let mut b = 1; let mut f = 0; for _ in 0..n-1 { f = a + b; a = b; b = f; } f }
a
, b
で前回と前々回の項を持っておき愚直に計算します。すると大きな n に対しては先ほどの実装よりも高速に値を計算できることが分かるかと思います。
次のように main 関数から呼び出すと共に 55 が出力されます。
fn main() { let n = 10; println!("{}", fibonacci_recursive(n)); println!("{}", fibonacci_dp(n)); }
ちなみに余談ですが、行列の繰り返し二乗法を用いることでさらに高速な計算が可能になります(「フィボナッチ数列 繰り返し二乗法」などで検索)。
クリスマスソングの出力
最後に The Twelve Days of Christmas という曲の歌詞を、その構造に注目して出力します。歌詞を検索すると分かりますが、反復した構造を持っています(積み上げ歌というらしいです)。for 文を使うとスマートに出力できそうです。歌詞のポイントは以下の通りです。
- 日が1番ごとに進む first -> second -> ... -> twelfth
- 同じ内容を繰り返しながら少しずつ歌詞が増えていく
- 積み上がる歌詞が複数ある時、最後の積み上げに and が付く
この構造に着目して実装していきます。cargo new --bin christmas
などでディレクトリを作成します。
まずは日数と積み上がる歌詞を定義します。ここは愚直に書きます。
let numbers = [ "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", ]; let objects = [ "a partridge in a pear tree", "two turtle doves", "three French hens", "four calling birds", "five golden rings", "six geese a-laying", "seven swans a-swimming", "eight maids a-milking", "nine ladies dancing", "ten lords a-leaping", "eleven pipers piping", "twelve drummers drumming", ];
次に歌詞の出力です。この曲の歌詞は「「日数のみ変化する出だし」->「積み上げ部分」を 12 回繰り返す」という構造になっており、12 回の繰り返しと積み上げ部分をそれぞれ for 文で書くことが出来ます。「積み上げ部分で何回繰り返すか」は「今何日目か」に依存しているので、日数のループ部分は配列の中身だけでなく現在のインデックスを取得できる enumerate
を使用します。
積み上げ部分は今までの歌詞の前に積み上がっていくのでイテレータを逆から見る .rev()
を使うと便利です。また初日以降の 'and' の出力を忘れずに行いましょう。これらをまとめると以下のようなコードになります。
fn main() { let numbers = [ "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", ]; let objects = [ "a partridge in a pear tree", "two turtle doves", "three French hens", "four calling birds", "five golden rings", "six geese a-laying", "seven swans a-swimming", "eight maids a-milking", "nine ladies dancing", "ten lords a-leaping", "eleven pipers piping", "twelve drummers drumming", ]; // (i, num) = (0, "first"), (1, "second"), ... for (i, num) in numbers.iter().enumerate() { println!("On the {} day of Christmas, my true love sent to me", num); for j in (0..=i).rev() { // Do not forget to print 'and' after day 1 if j == 0 && i > 0 { print!("and "); } println!("{}", objects[j]); } println!(); } }
cargo run
で実行すると所望の歌詞が得られることが分かります。
終わりに
Rust の勉強がてら簡単な記事を書いてみました。これから更に高度なことにも挑戦していこうと思います。