LoginSignup
3
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

🦀100 Exercises To Learn Rustに挑戦【2】 if・パニック・演習

Last updated at Posted at 2024-06-10

前の記事

100 Exercise To Learn Rust 演習第2回になります!今回はif式について、パニックについてと、ここまでをまとめた演習、という題目になっています!

今回の関連ページ

では行きましょう!

[02_basic_calculator/03_if_else] if式

問題はこちらです。

lib.rs
/// Return `true` if `n` is even, `false` otherwise.
fn is_even(n: u32) -> bool {
    todo!()
}

// テスト省略
テスト込みver
lib.rs
/// Return `true` if `n` is even, `false` otherwise.
fn is_even(n: u32) -> bool {
    todo!()
}

#[cfg(test)]
mod tests {
    use crate::is_even;

    #[test]
    fn one() {
        assert!(!is_even(1));
    }

    #[test]
    fn two() {
        assert!(is_even(2));
    }

    #[test]
    fn high() {
        assert!(!is_even(231));
    }
}

n が偶数なら true を、 n が奇数なら false をそれぞれ返せというものです。

解説

Rustであることを忘れ、ifを使って愚直に書いてみるとこんな感じでしょうか?

Rust
fn is_even(n: u32) -> bool {
    let result;

    if n % 2 == 0 {
        result = true;
    } else {
        result = false;
    }

    return result;
}

しかしなんとも色々ツッコミたいソースコードです。良い機会ですのでClippyというリンターツールを使ってみたいと思います。

コマンド
cargo clippy
出力
    Checking if_else v0.1.0 (/path/to/100-exercises-to-learn-rust/exercises/02_basic_calculator/03_if_else)
warning: function `is_even` is never used
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:2:4
  |
2 | fn is_even(n: u32) -> bool {
  |    ^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: unneeded `return` statement
  --> exercises/02_basic_calculator/03_if_else/src/lib.rs:11:5
   |
11 |     return result;
   |     ^^^^^^^^^^^^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return
   = note: `#[warn(clippy::needless_return)]` on by default
help: remove `return`
   |
11 -     return result;
11 +     result
   |

warning: unneeded late initialization
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:3:5
  |
3 |     let result;
  |     ^^^^^^^^^^^
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
  = note: `#[warn(clippy::needless_late_init)]` on by default
help: declare `result` here
  |
5 |     let result = if n % 2 == 0 {
  |     ++++++++++++
help: remove the assignments from the branches
  |
6 ~         true
7 |     } else {
8 ~         false
  |
help: add a semicolon after the `if` expression
  |
9 |     };
  |      +

warning: this if-then-else expression assigns a bool literal
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:5:5
  |
5 | /     if n % 2 == 0 {
6 | |         result = true;
7 | |     } else {
8 | |         result = false;
9 | |     }
  | |_____^ help: you can reduce it to: `result = n % 2 == 0;`
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_bool_assign
  = note: `#[warn(clippy::needless_bool_assign)]` on by default

warning: `if_else` (lib) generated 4 warnings (run `cargo clippy --fix --lib -p if_else` to apply 2 suggestions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s

1つ目の warning: function 'is_even' is never used は無視してしまって構わないです。

  • unneeded 'return' statement
    • Rustは 式指向言語 ですから関数の最後の return は不要です
  • unneeded late initialization
    • 同じく、式指向言語のため、 if構文も式 (Exercises内での解説) ですから result の初期化を各節で行う必要はありません

というわけで(最後のを見なかったことにして)ここまでを踏まえるとこんな感じに直せます!

lib.rs
fn is_even(n: u32) -> bool {
    if n % 2 == 0 {
        true
    } else {
        false
    }
}
冗長な書き方
lib.rs
fn is_even(n: u32) -> bool {
    let result = if n % 2 == 0 {
        true
    } else {
        false
    };

    result
}
  • ifは式なので result に返り値を束縛できます (冗長な書き方として折りたたんでいます)
  • 関数は最後に評価された値を返すので、そもそも result への束縛は要らないことに気づきます

まとめると、上記コードにより関数の返り値はif式評価後の値となり、n の偶奇判定を返せました!

... :thinking: ... :bulb: :astonished:
コマンドと出力
$ cargo clippy
    Checking if_else v0.1.0 (/path/to/100-exercises-to-learn-rust/exercises/02_basic_calculator/03_if_else)
warning: function `is_even` is never used
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:2:4
  |
2 | fn is_even(n: u32) -> bool {
  |    ^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: this if-then-else expression returns a bool literal
 --> exercises/02_basic_calculator/03_if_else/src/lib.rs:3:5
  |
3 | /     if n % 2 == 0 {
4 | |         true
5 | |     } else {
6 | |         false
7 | |     }
  | |_____^ help: you can reduce it to: `n % 2 == 0`
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_bool
  = note: `#[warn(clippy::needless_bool)]` on by default

warning: `if_else` (lib) generated 2 warnings (run `cargo clippy --fix --lib -p if_else` to apply 1 suggestion)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
Rust
fn is_even(n: u32) -> bool {
    n % 2 == 0
}

そもそもif式要らないじゃん...!if式の問題だと思って先入観に囚われていました...

ちなみに模範解答の方もif式は使ってないです。こういう問題にしたのはわざと?

こういう時Clippyは偉大です...!

[02_basic_calculator/04_panics] パニック

問題はこちらです。長いですが解くのに必要な情報を含むためテスト部分まで載せます。

lib.rs
/// Given the start and end points of a journey, and the time it took to complete the journey,
/// calculate the average speed of the journey.
fn speed(start: u32, end: u32, time_elapsed: u32) -> u32 {
    // TODO: Panic with a custom message if `time_elapsed` is 0

    (end - start) / time_elapsed
}

#[cfg(test)]
mod tests {
    use crate::speed;

    #[test]
    fn case1() {
        assert_eq!(speed(0, 10, 10), 1);
    }

    #[test]
    // 👇 With the `#[should_panic]` annotation we can assert that we expect the code
    //    under test to panic. We can also check the panic message by using `expected`.
    //    This is all part of Rust's built-in test framework!
    #[should_panic(expected = "The journey took no time at all, that's impossible!")]
    fn by_zero() {
        speed(0, 10, 0);
    }
}

まず前提として、 speed 関数は0割り除算が生じるとパニックになります。それ自体は良いのですがその時はテスト側で指定している The journey took no time at all, that's impossible! というカスタムメッセージを出してほしいようです。

Rust playground

https://play.rust-lang.org/

Exercisesの方で紹介されていたのでこちらでも紹介しておきます。WandboxideoneTS playgroundみたいなオンラインエディタのRust特化版です!

Wandbox同様パーマリンクでソースコードを共有できるので、プロジェクトを作成せずに軽く文法を確認したい時や、SNSとかでRustについて語る時に便利だったりします。是非使ってみましょう!

解説

time_elapsed == 0 だったら前もってパニックさせて、そこで表示させたい文言を出力してしまいます。if式と panicマクロで実現可能です。 (こっちの問題こそとりあえずifがふさわしいのでは...?)

lib.rs
fn speed(start: u32, end: u32, time_elapsed: u32) -> u32 {
    // TODO: Panic with a custom message if `time_elapsed` is 0

    if time_elapsed == 0 {
        panic!("The journey took no time at all, that's impossible!");
    }

    (end - start) / time_elapsed
}

ちなみにこういうときは assert_* 系のマクロが便利です!time_elapsed != 0 であることを確かめたいので、今回は assert_ne マクロが使えます。

lib.rs
fn speed(start: u32, end: u32, time_elapsed: u32) -> u32 {
    // TODO: Panic with a custom message if `time_elapsed` is 0

    assert_ne!(
        time_elapsed, 0,
        "The journey took no time at all, that's impossible!"
    );

    (end - start) / time_elapsed
}

assert_* は「条件を満たしていることを保証している」と見ると読みやすいです。

  • assert_eq : 第一引数と第二引数が等しいことを保証したい
    • 等しくないとパニックになる
  • assert_ne : 第一引数と第二引数が等しくないことを保証したい
    • 等しいとパニックになる

使い慣れてないと混乱しやすいですね...そして第三引数はパニック時のカスタムメッセージを指定できるので、ここにテストで求められている文言を指定してあげればよいです。

行数的にはifを使う場合とあまり変わらない(どころか1行多い)ですが、「保証」のニュアンスが出るためこっちのほうが読みやすいという人も一定数はいるんじゃないかと思います。

Q: Result型を使うと良いと聞いたのだけど...?

A: はい。こういうエラーになる状況では、いきなりパニックにするのではなくてプログラム側でエラー時の処理をカスタマイズできる Result を使うほうが望ましいです。(100 Exercisesだと中盤で紹介されているみたい...)

慣れないうちはできるだけ Result 型にしておけば確かに間違いないですが、パニック系統も適切な場面で使えるようになるのが理想です。以下、パニックする書き方( もとい unwrap 系メソッド )が有効的な例を示します。

  • コンパイル時に値が確定しており パニックしないことが火を見るより明らかな場合
    • unwrap を使った方が読みやすくなります
  • 環境変数や前提となる依存ファイルを main 関数序盤で読み込むような場合
    • Result による分岐は冗長となることがしばしばです。こういう時は expect を使うといい感じになります

Result 型はユーザー入力の処理といった「実行時まで入力が確定しない場合」に使うという意識を持っておくと適切に扱えるようになる気がします。大体のプログラムはそういうものなので(偏見)、Result型が適切という場合が多いのでしょう。

[02_basic_calculator/05_factorial] 関数を書いてみる

問題は...テスト以外コメントしかなかったのでコメントを以下にまとめます。

  • 非負整数 n を受け取る関数 factorial を定義し、nの階乗( n! )を返すようにしてください
  • 階乗は、n までのすべての正の整数の積です。例: 5! = 5 * 4 * 3 * 2 * 1 = 120
    • 0!1 と定義されています
  • factorial(0)1 を返し、 factorial(1)1 を返し、 factorial(2)2 を返し...としてください
  • ここまで習ったことを使ってください。ループはまだ習ってませんね、再帰を使いましょう!
問題コード全体
lib.rs
// Define a function named `factorial` that, given a non-negative integer `n`,
// returns `n!`, the factorial of `n`.
//
// The factorial of `n` is defined as the product of all positive integers up to `n`.
// For example, `5!` (read "five factorial") is `5 * 4 * 3 * 2 * 1`, which is `120`.
// `0!` is defined to be `1`.
//
// We expect `factorial(0)` to return `1`, `factorial(1)` to return `1`,
// `factorial(2)` to return `2`, and so on.
//
// Use only what you learned! No loops yet, so you'll have to use recursion!

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}

いや確かにRustではまだループ習ってないけどだからって再帰はいいのかい...まぁいいや

解説

再帰を止めるためのガード節と、再帰による評価部分を書けばいいだけです。

lib.rs
fn factorial(n: u32) -> u32 {
    if n == 0 {
        return 1;
    }

    n * factorial(n - 1)
}

今回までの問題のまとめとしては適切でしょう。

  • 文法
    • 関数を定義するという形で復習になっています
  • 非負整数
    • u32 型にしてしまえば負数が引数に渡される心配はなく、特に例外処理を追加する必要はありません
  • 変数
    • 関数の引数という形で登場しています
  • if式パニック
    • 0 が引数として渡された時に n - 1 が実行されると パニックになります がガード節で弾いているので問題なしです
    • 筆者の書き方では式要素がないですが、模範解答の方は式であることの理解を求めているようです1

振り返ってみると良くできた問題でびっくりします。


ループ等本格的な文法はまだ出ていないのに、最後はなかなか興味深い問題でしたね。

では次の問題に行きましょう!

次の記事: 【3】 可変・ループ・オーバーフロー

  1. 筆者はこういう時ガード節の書き方のほうが好きです。注釈お気持ち表明

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0