// book chapter
動かして学ぶRustプログラミング問題集第4章 関数.
この章では、処理をひとまとまりの部品として切り出す関数を学びます。
関数の定義と呼び出しから、引数・戻り値・早期リターンまでを8問で身につけます。
第3章まではすべての処理をmainの中に書いてきました。関数が使えるようになると、同じ処理を何度も書かずに済み、名前を付けることでコードの意図も伝わりやすくなります。
進め方は第1章から第3章までと同じですが、この章の後半では新しくテストで正解を判定する問題が登場します。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - 関数を定義する
関数に関する問題です。
「いらっしゃいませ。」と出力する関数greetを定義し、mainから2回呼び出してください。
いらっしゃいませ。
いらっしゃいませ。fn main() {
// greetを2回呼び出せ
}
// 「いらっしゃいませ。」と出力する関数greetを定義せよPlaygroundで開く解答例と解説
fn main() {
// greetを2回呼び出せ
greet();
greet();
}
// 「いらっしゃいませ。」と出力する関数greetを定義せよ
fn greet() {
println!("いらっしゃいませ。");
} Playgroundで開く関数はfn 関数名() { ... }の形で定義し、関数名()の形で呼び出します。呼び出すたびに本体のブロックが実行されるので、同じ処理を何度でも使い回せます。
関数名はgreetやprint_totalのように、単語を小文字とアンダースコアでつなぐスネークケースで書くのが慣習です。printTotalのようなキャメルケースで書くとコンパイルは通りますが、警告が出ます。
greetをmainより後ろに定義している点にも注目してください。関数は「アイテム」と呼ばれる宣言の一種で、ファイル内のどこに書いても構いません。mainの前に書いても後ろに書いても同じように動きます。
02 - 引数を渡す
関数に関する問題です。
次のコードは単価と個数から合計金額を出力しようとしていますが、コンパイルエラーになります。エラーメッセージを読み、print_totalの1行目だけを修正してコンパイルが通るようにしてください。
合計は600円です。fn print_total(price, count) {
println!("合計は{}円です。", price * count);
}
fn main() {
print_total(150, 4);
}Playgroundで開く解答例と解説
fn print_total(price, count) {
fn print_total(price: u32, count: u32) { // 引数それぞれに型注釈を付ける
println!("合計は{}円です。", price * count);
}
fn main() {
print_total(150, 4);
}Playgroundで開くエラーメッセージは「expected one of :, @, or |, found ,」で、続けて「if this is a parameter name, give it a type(これが引数名なら型を付けてください)」というヒントが出ていました。
関数に値を渡すには、fn 関数名(引数名: 型, 引数名: 型)のように引数を宣言します。呼び出し側でprint_total(150, 4)と書くと、150がpriceに、4がcountに順に束縛されます。
ここでのポイントは、引数の型注釈は省略できないことです。letでは型注釈を省略してもコンパイラが値から型を推論してくれましたが、関数の引数では必ず型を書く必要があります。関数の内側と外側それぞれを独立して検査できるようにするための決まりで、引数の型が書いてあれば、呼び出し側のコードを見なくても関数の中身を検査できます。
型はu32でなくても、i32のようにprice * countが計算できる型であれば正解です。ただし2つの引数は同じ型に揃えてください。第2章で学んだとおり、i32とu32のように型が違う値同士は掛け算できません。
03 - 戻り値は末尾の式
関数と式に関する問題です。
幅と高さを受け取り、長方形の面積を返す関数rectangle_areaを定義してください。
面積は40です。fn main() {
let area = rectangle_area(8, 5);
println!("面積は{}です。", area);
}
// 幅widthと高さheightを受け取り、面積を返す関数rectangle_areaを定義せよPlaygroundで開く解答例と解説
fn main() {
let area = rectangle_area(8, 5);
println!("面積は{}です。", area);
}
// 幅widthと高さheightを受け取り、面積を返す関数rectangle_areaを定義せよ
fn rectangle_area(width: u32, height: u32) -> u32 {
width * height
} Playgroundで開く値を返す関数は、引数の後ろに-> 型と書いて戻り値の型を宣言します。そして本体ブロックの末尾に置いたセミコロンなしの式が、そのまま戻り値になります。
第1章で学んだ「ブロックは末尾の式の値を返す」という性質が、そのまま関数にも当てはまります。returnを書く必要はありません(returnの出番は問題05で扱います)。
戻り値の型は1つだけしか書けません。複数の値を返したくなった場合は、第5章で学ぶタプルにまとめて返します。
04 - セミコロンの罠
関数・文・式に関する問題です。
次のコードは受け取った値を2倍にして返そうとしていますが、コンパイルエラー(E0308)になります。エラーメッセージを読み、1文字だけ変更してコンパイルが通るようにしてください。
21の2倍は42です。fn double(value: i32) -> i32 {
value * 2;
}
fn main() {
println!("21の2倍は{}です。", double(21));
}Playgroundで開く解答例と解説
fn double(value: i32) -> i32 {
value * 2;
value * 2 // セミコロンを外すと式のまま残り、戻り値になる
}
fn main() {
println!("21の2倍は{}です。", double(21));
}Playgroundで開くエラーメッセージは「mismatched types」、続けて「expected i32, found ()」でした。さらに「implicitly returns () as its body has no tail or return expression(本体に末尾の式もreturnもないため、暗黙に()を返している)」という説明と、「remove this semicolon to return this value」という修正案まで示されています。
value * 2は値を生む式ですが、末尾にセミコロンを付けると値を捨てる文になります。文で終わったブロックの値はユニット型()になるため、「i32を返すと宣言したのに()を返している」という型の不一致になっていたわけです。
このセミコロン1つの違いは、Rustを書き始めた頃につまずきやすいポイントです。「値を返したい行にはセミコロンを付けない」と覚えておいてください。
05 - return式で早期リターン
return式と関数に関する問題です。
入場料を求める関数admission_feeを完成させてください。大人料金は1800円ですが、12歳以下は無料です。12歳以下の場合はreturnで関数を抜けるように書いてください。
10歳の入場料: 0円
30歳の入場料: 1800円fn admission_fee(age: u32) -> u32 {
// ageが12以下なら0を返して関数を抜けよ
1800
}
fn main() {
println!("10歳の入場料: {}円", admission_fee(10));
println!("30歳の入場料: {}円", admission_fee(30));
}Playgroundで開く解答例と解説
fn admission_fee(age: u32) -> u32 {
// ageが12以下なら0を返して関数を抜けよ
if age <= 12 {
return 0; // ここで関数を抜けるので、以降は実行されない
}
1800
}
fn main() {
println!("10歳の入場料: {}円", admission_fee(10));
println!("30歳の入場料: {}円", admission_fee(30));
}Playgroundで開くreturn 値;と書くと、その時点で関数の実行を打ち切って呼び出し元に値を返します。条件を満たしたときだけ先に抜ける、この書き方を早期リターンと呼びます。
問題03で見たとおり、Rustでは末尾の式がそのまま戻り値になるため、最後に値を返すだけならreturnは不要です。実際return 1800;と書いても動きますが、末尾は1800とだけ書くのがRustの慣習です。途中で抜けるときはreturn、最後に返すときは末尾の式、と使い分けてください。
なお、この関数はif式を使って次のようにも書けます。どちらも正解です。
fn admission_fee(age: u32) -> u32 {
if age <= 12 { 0 } else { 1800 }
}
fn main() {
println!("10歳の入場料: {}円", admission_fee(10));
println!("30歳の入場料: {}円", admission_fee(30));
}Playgroundで開く条件が1つならこの書き方で十分ですが、条件のネストが深くなる場合や、抜けた後にも長い処理が続く場合は、早期リターンのほうが読みやすくなります。
06 - ユニット型
ユニット型と関数に関する問題です。
次のコードは価格と税込価格を出力しようとしていますが、コンパイルエラー(E0369)になります。エラーメッセージを読み、show_priceの定義は変えずにmainだけを修正してください。
価格は500円です。
税込は550円です。fn show_price(price: u32) {
println!("価格は{}円です。", price);
}
fn main() {
let price = show_price(500);
let with_tax = price + price / 10;
println!("税込は{}円です。", with_tax);
}Playgroundで開く解答例と解説
fn show_price(price: u32) {
println!("価格は{}円です。", price);
}
fn main() {
let price = show_price(500);
let price = 500; // 値そのものを束縛する
show_price(price); // 出力だけを関数に任せる
let with_tax = price + price / 10;
println!("税込は{}円です。", with_tax);
}Playgroundで開くエラーメッセージは「cannot divide () by {integer}」、つまり「()を整数で割ることはできない」でした。priceには500ではなく()が入っていた、ということです。
show_priceは戻り値の型->を書いていません。戻り値の型を省略した関数は()(ユニット型)を返す決まりなので、fn show_price(price: u32)はfn show_price(price: u32) -> ()と書いたのと同じ意味になります。()は「返すべき意味のある値がない」ことを表す型で、値も()の1つしかありません。
let price = show_price(500);が受け取っていたのは、出力された500ではなく、この()でした。println!が画面に文字を出すことと、関数が値を返すことは別の話だという点がポイントです。
解答例では、値をmain側で持っておき、関数には出力だけを任せました。もう1つの直し方は、show_priceが値を返すようにすることです。
fn show_price(price: u32) -> u32 {
println!("価格は{}円です。", price);
price // 受け取った値をそのまま返す
}
fn main() {
let price = show_price(500);
let with_tax = price + price / 10;
println!("税込は{}円です。", with_tax);
}Playgroundで開く07 - テストに合格する関数を書く
関数に関する問題です。
ここからはテストで正解を判定する問題です。あらかじめ用意されたテストコードに合格するように、関数is_evenを実装してください。is_evenは受け取った整数が偶数ならtrue、奇数ならfalseを返します。
コードにはfn mainがありません。Playgroundの「RUN」の隣にある「TEST」(または「···」メニュー)を選んで実行し、すべてのテストがokになれば正解です。
// 受け取った整数が偶数ならtrue、奇数ならfalseを返す関数is_evenを定義せよ
#[test]
fn test_is_even() {
assert_eq!(is_even(4), true);
assert_eq!(is_even(7), false);
assert_eq!(is_even(0), true);
assert_eq!(is_even(-3), false);
}Playgroundで開く解答例と解説
// 受け取った整数が偶数ならtrue、奇数ならfalseを返す関数is_evenを定義せよ
fn is_even(number: i32) -> bool {
number % 2 == 0
}
#[test]
fn test_is_even() {
assert_eq!(is_even(4), true);
assert_eq!(is_even(7), false);
assert_eq!(is_even(0), true);
assert_eq!(is_even(-3), false);
}Playgroundで開く偶数の判定は、第2章の剰余演算子%で「2で割った余りが0か」を調べます。number % 2 == 0はboolを生む式なので、これをそのまま末尾に置けば戻り値になります。
テストの読み方も確認しておきましょう。
#[test]は、その直後の関数がテスト用の関数であることを示す目印です。テスト実行時だけ呼び出されますassert_eq!(左, 右)は「左と右が等しいこと」を確かめるマクロです。等しくなければテストが失敗し、期待値と実際の値が表示されます
テストコードは書き換えずに、関数側を実装して合格させてください。テストは「この関数はこう動くべきだ」という仕様書でもあるので、まずテストを読んで何を作るのかを把握するのが定石です。
引数の型はi32でなくても構いませんが、テストが-3を渡しているので符号なしのu32にすると失敗します。テストを読めば必要な型が分かる、という一例です。
08 - 応用: 温度変換関数
第4章の総復習として、関数・数値演算・キャストを組み合わせた問題です。
摂氏(i32)を受け取って華氏(f64)に変換して返す関数celsius_to_fahrenheitを実装し、テストに合格させてください。摂氏から華氏への変換式は「華氏 = 摂氏 × 9 ÷ 5 + 32」です。
// 摂氏celsius(i32)を華氏(f64)に変換して返す関数celsius_to_fahrenheitを定義せよ
#[test]
fn test_celsius_to_fahrenheit() {
assert_eq!(celsius_to_fahrenheit(0), 32.0);
assert_eq!(celsius_to_fahrenheit(100), 212.0);
assert_eq!(celsius_to_fahrenheit(-40), -40.0);
}Playgroundで開く解答例と解説
// 摂氏celsius(i32)を華氏(f64)に変換して返す関数celsius_to_fahrenheitを定義せよ
fn celsius_to_fahrenheit(celsius: i32) -> f64 {
celsius as f64 * 9.0 / 5.0 + 32.0
}
#[test]
fn test_celsius_to_fahrenheit() {
assert_eq!(celsius_to_fahrenheit(0), 32.0);
assert_eq!(celsius_to_fahrenheit(100), 212.0);
assert_eq!(celsius_to_fahrenheit(-40), -40.0);
}Playgroundで開く引数はi32、戻り値はf64と型が違うので、第2章で学んだキャストの出番です。celsius as f64でf64に揃えてから、9.0・5.0・32.0という小数リテラルと計算します。
やってしまいがちな間違いが2つあります。
1つ目は、キャストを忘れてcelsius * 9 / 5 + 32と書いてしまうことです。これは整数のまま計算されるうえ、戻り値の型f64とも合わずコンパイルエラーになります。
2つ目は、celsius as f64 * (9 / 5) + 32.0のように整数のまま割ってしまうことです。9 / 5は整数同士の除算なので、切り捨てられて1になってしまいます。この場合はコンパイルが通ってしまい、摂氏100度が132度という誤った結果になります。コンパイルが通ることと、正しく動くことは別だと分かる例です。
計算の順序を変えて(celsius as f64 - 0.0) * 1.8 + 32.0のように書いても、テストは通ります。
これで第4章は終わりです。処理を関数として切り出せるようになりました。次の章では、複数の値をひとまとめに扱うタプル・配列・スライスに進みます。