4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Rust 100 Ex 🏃【3/37】 可変・ループ・オーバーフロー

Last updated at Posted at 2024-06-13

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第3回になります!ループ構造2つと、前々回 に関連しているかもしれないオーバーフローの話になります!

今回の関連ページ

では行きましょう!

[02_basic_calculator/06_while] while

問題はこちらです。

lib.rs
// Rewrite the factorial function using a `while` loop.
pub fn factorial(n: u32) -> u32 {
    // The `todo!()` macro is a placeholder that the compiler
    // interprets as "I'll get back to this later", thus
    // suppressing type errors.
    // It panics at runtime.
    todo!()
}
テストを含めた全体
lib.rs
// Rewrite the factorial function using a `while` loop.
pub fn factorial(n: u32) -> u32 {
    // The `todo!()` macro is a placeholder that the compiler
    // interprets as "I'll get back to this later", thus
    // suppressing type errors.
    // It panics at runtime.
    todo!()
}

#[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);
    }
}

前回、階乗を求めるコードを再帰で書きましたが、これをwhileループで書くという問題のようです。

todo! マクロはまだ未実装であることを示し、ここに到達するとコードがパニックするようになっています。多分いつかの回で取り扱うのでしょうが、この todo! マクロは「関数の返り値と同じ型の値を返した」ことにできる特殊な性質があるため、コンパイルエラーにならなかったりします。

ともかく、この部分を置き換えてwhileループにすれば良さそうです。

解説

lib.rs
pub fn factorial(n: u32) -> u32 {
    let mut res = 1;

    let mut i = 1;
    while i <= n {
        res *= i;
    
        i += 1;
    }

    res
}

... めちゃくちゃfor文で書きたい ...while文をこんな風に無理に使うことがないため、逆にテンパってしまいました...

この問題で新しく出てきている要素は2つあります。

  • while文
  • mut キーワード

この節でついでに「可変」を登場させているみたいですね。実際、こういうループ構造で良く使うキーワードだったりするので理にかなっています。

Rustではデフォルトですべての変数は 不変 です。そして意図的に仕組まない限り1、この不変性は構造体から参照、配列まで至るところで保たれています。値が変わらないことはコードを読む人に安心感を与え、「この関数で可変参照渡しされてるんじゃなかろうか...」といった余計な詮索をする必要がない明確なコーディングを実現しています!

しかし全てが不変だと不便な時があり、その代表が繰り返し変数です。そしてこういう可変な変数を用いたいときのために mut キーワードがあります。このキーワードを書くことで「この変数は副作用を受けて変化することがある」と読み手に常に明示的に伝えることができるのです!

話をwhileループの方に戻しましょう。(if式もそうでしたが)条件節をカッコで包んでいない以外は特に他言語との違いは無いんじゃないかと思います。if式と異なり、評価されたところで返す値が決まらないため式としての性質は持たないです(伏線)。

i++はないの?

インクリメント構文はありません。この辺が理由じゃないかなと思います → Rustでは++すら許されない #C++ - Qiita

そもそも i++ は一つの構文で複数のことをやりすぎです。横着要素はあまり好まない(導入に慎重な)言語ゆえにわざと用意していないと考えられます。

[02_basic_calculator/07_for] for

問題はこちらです。やっと for で書かせてもらえる...!

lib.rs
// Rewrite the factorial function using a `for` loop.
pub fn factorial(n: u32) -> u32 {
    todo!()
}
テストを含めた全体
lib.rs
// Rewrite the factorial function using a `for` loop.
pub fn factorial(n: u32) -> u32 {
    todo!()
}

#[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);
    }
}

こうして何問もお題として使えるって、階乗は優秀ですね...

解説

lib.rs
pub fn factorial(n: u32) -> u32 {
    let mut res = 1;
    for i in 1..=n {
        res *= i;
    }
    res
}

繰り返し変数のスコープをforループに閉じ込めることができており、whileループよりもスッキリしたコードになっています!imut が要らないお陰で、各ループ内では i が変化しないことが保証されていて非常に良いですね。

ただしまだ可変変数 res が残っていますね...これすらもイテレータ的な書き方( mapreduce を使う等)をすればなくすことができたりします。

後ほど登場するであろうその「イテレータ」に関係していますが、1..=51, 2, 3, 4, 5 を生成する Range という型の値で、for文に特有の構文というわけでありません。for文はあくまでも for 変数 in イテレータ {} という構文であり、 Range 型等のイテレータから要素を一つずつ取り出して処理しています。

loop式

whileやforは値を返さないから式ではなかったのですが、一方でloop式は値を返すことが可能です(伏線回収)。

loop式は loop {} と書くのですが、これは while true {} 、すなわち無限ループを意味する専用構文です。

無限ループ専用ゆえに抜け出すにはbreakが必須なため、breakを使用することで値を返すことができる仕様となっています。

Rust
let mut i = 0;

let res = loop {
    if i == 10 {
        break i;
    }

    println!("{}", i);

    i += 1;
};

println!("{}", res);

break i;で、抜け出した時に式としてiの中身を返しています!

...まぁ使用機会は少ないと思います。おもしろ構文だったので紹介してみました

[02_basic_calculator/08_overflow] オーバーフロー

問題はこちらです。

lib.rs
// Customize the `dev` profile to wrap around on overflow.
// Check Cargo's documentation to find out the right syntax:
// https://doc.rust-lang.org/cargo/reference/profiles.html
//
// For reasons that we'll explain later, the customization needs to be done in the `Cargo.toml`
// at the root of the repository, not in the `Cargo.toml` of the exercise.

pub fn factorial(n: u32) -> u32 {
    let mut result = 1;
    for i in 1..=n {
        result *= i;
    }
    result
}

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

    #[test]
    fn twentieth() {
        // 20! is 2432902008176640000, which is too large to fit in a u32
        // With the default dev profile, this will panic when you run `cargo test`
        // We want it to wrap around instead
        assert_eq!(factorial(20), 2_192_834_560);
        //                           ☝️
        // A large number literal using underscores to improve readability!
    }

    // ...省略...
}
省略なし
lib.rs
// Customize the `dev` profile to wrap around on overflow.
// Check Cargo's documentation to find out the right syntax:
// https://doc.rust-lang.org/cargo/reference/profiles.html
//
// For reasons that we'll explain later, the customization needs to be done in the `Cargo.toml`
// at the root of the repository, not in the `Cargo.toml` of the exercise.

pub fn factorial(n: u32) -> u32 {
    let mut result = 1;
    for i in 1..=n {
        result *= i;
    }
    result
}

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

    #[test]
    fn twentieth() {
        // 20! is 2432902008176640000, which is too large to fit in a u32
        // With the default dev profile, this will panic when you run `cargo test`
        // We want it to wrap around instead
        assert_eq!(factorial(20), 2_192_834_560);
        //                           ☝️
        // A large number literal using underscores to improve readability!
    }

    #[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);
    }
}

ってさっきの問題の答えが書いてる...!?

そして何をすればよいかわかりにくい問題ですが、どうやら「 20! = 2432902008176640000 は大きすぎるからオーバーフローによりパニックしちゃうので、パニックしないように Cargo.toml の設定を書き換えて! 」という問題らしいです。

解説

プロジェクトのCargo.toml (exercises内の問題ごとのCargo.tomlではなく、大本のCargo.tomlです) の方に、オーバーフロー発生時に無視する設定を追記します。

Cargo.toml
[workspace]
members = ["exercises/*/*", "helpers/common", "helpers/mdbook-exercise-linker", "helpers/ticket_fields"]
resolver = "2"

+ [profile.dev]
+ overflow-checks = false

...いや、無理やりすぎるだろ...?!ちなみに dev (デバッグビルド) だと overflow-checks のデフォルト値は true な一方で、 release (リリースビルド) だとデフォルト値は false になっています。AtCoderでRustを選択して解答するとリリースビルドが行われるため、手元(デバッグビルド)だとオーバーフローのせいでRE (ランタイムエラー) になるケースが、回答送信後にWA (誤答) で表示されるという初見殺しがあるのですが、つまりプロファイルのデフォルト値が原因なわけです。

オーバーフローへの対策には色々ありますね。AtCoderで回答する際は 10e9+7 等の素数で剰余を取ったりしますし、筆者が専攻していた暗号学では「有限体」というものを用いて全ての演算結果が必ず有限集合(体)の中に収まるような計算しか取り扱わなかったりします2

素数での剰余を取るわけではないので演算の整合性が取れなくなる気がしますが、本問のテストケースは単に溢れたらまた0からカウントしている(wrap aroundしている)実装になってそうです。Cargo.tomlを編集してオーバーフローが検知できなくなるのは避けたいので、wrap aroundしていることを踏まえて別解も考えてみました。

lib.rs
pub fn factorial(n: u32) -> u32 {
    let mut result = 1u32;
    for i in 1..=n {
        result = result.wrapping_mul(i);
    }
    result
}

u32::wrapping_mul を使えばwrap aroundに演算を行ってくれ、テストを通すことができます!ソースコード側でオーバーフローに配慮されていることを明記していたほうが、一々Cargo.tomlを見なくて済んで良さそうですね。

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

次の記事: 【4】 キャスト・構造体 (たまにUFCS)

登場したPlayground

(実際に無効化したことはないですが、)Rust Playground上のデータが喪失する可能性を鑑みて、一応記事にもソースコードを掲載することとしました。

URL: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=2df7c0d72790feede86cf0f9082c298c

Rust
#[derive(Debug)]
struct Hoge<'a>(&'a mut i32);

fn main() {
    let mut n = 10_i32;
    
    let hoge = Hoge(&mut n);
    
    println!("{:?}", hoge);
    
    *hoge.0 = 5;
    
    println!("{:?}", hoge);
}
  1. 例えば「可変参照」を内側に持つ構造体を不変として束縛するといったことをすれば例外的に可変要素を持つ不変な値を作ることはできます。できますがまず自分で書くことはないです。

  2. もちろん、有限体ではなく普通に実数を使う暗号なんかもあったりしますが、筆者は有限体を用いる暗号が好きでした。

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?