Rust dojo第59回を開催した

第59回です。前回はこちら。

kdnakt.hatenablog.com

 

 

[第59回の様子]

2022/11/30に第59回を開催した。

内容としてはRust By Example 日本語版の18. エラーハンドリングの「18.3.3. 早期リターン」〜「18.4.2. エラー型を定義する」に取り組んだ。

参加者は自分を入れて6人くらい。勤労感謝の日で1週あいたけどみんな来てくれてよかった。

 

[学んだこと]

  • 18.3.3. 早期リターン
  • 前回はmap()やand_then()を用いてエラーハンドリングを行なった
  • ここではmatchと早期リターンの組み合わせを見ていく
  • parse()が返すResultをmatchで処理すると以下のようになる
use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = match first_number_str.parse::<i32>() {
        Ok(first_number)  => first_number,
        Err(e) => return Err(e),
    };

    let second_number = match second_number_str.parse::<i32>() {
        Ok(second_number)  => second_number,
        Err(e) => return Err(e),
    };

    Ok(first_number * second_number)
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2")); // n is 20
    print(multiply("t", "2")); // Error: invalid digit found in string
}
  • ちょっと冗長だし、first_number_strとsecond_number_strのどちらがエラーになったのか分かりづらいのでリターンする内容はもう少し工夫できそうな気もする
  • 18.3.4. ?の導入
  • Errを取り出すのに先ほどmatchを利用したが、?を利用するとより簡潔に書ける
use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = first_number_str.parse::<i32>()?;
    let second_number = second_number_str.parse::<i32>()?;

    Ok(first_number * second_number)
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));// n is 20
    print(multiply("t", "2"));// Error: invalid digit found in string
}
  • ?が実装されるまでは同様の動作をtry!マクロで実装していた:古いコードで見られる
use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = try!(first_number_str.parse::<i32>());
    let second_number = try!(second_number_str.parse::<i32>());

    Ok(first_number * second_number)
}
  • try!マクロを利用する際はCargoのパッケージバージョンを2015に変更する必要があるらしい
    • 最新版は2021なのでなかなか古い。
  • 18.4. 複数のエラー型
  • OptionとResultを組み合わせたり、異なるErrのResultを組み合わせる方法を学んでいく
  • 例題として、配列の最初の要素を2倍して返す以下の関数を扱う
fn double_first(vec: Vec<&str>) -> i32 {
    let first = vec.first().unwrap();    // `Option::unwrap()` on a `None` value'の可能性
    2 * first.parse::<i32>().unwrap() // ParseIntErrorの可能性
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("The first doubled is {}", double_first(numbers));
    println!("The first doubled is {}", double_first(empty));
    println!("The first doubled is {}", double_first(strings));
}
use std::num::ParseIntError;

fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> {
    vec.first().map(|first| {
        first.parse::<i32>().map(|n| 2 * n)
    })
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("The first doubled is {:?}", double_first(numbers)); // Some(Ok(84))
    println!("The first doubled is {:?}", double_first(empty));   // None
    println!("The first doubled is {:?}", double_first(strings)); // Some(Err(ParseIntError { kind: InvalidDigit }))
}
  • 反対に、OptionをResultに埋め込むこともできる
use std::num::ParseIntError;

fn double_first(vec: Vec<&str>) -> Result<Option<i32>, ParseIntError> {
    let opt = vec.first().map(|first| {
        first.parse::<i32>().map(|n| 2 * n)
    });

    opt.map_or(Ok(None), |r| r.map(Some))
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("The first doubled is {:?}", double_first(numbers)); // Ok(Some(84))
    println!("The first doubled is {:?}", double_first(empty));   // Ok(None)
    println!("The first doubled is {:?}", double_first(strings)); // Err(ParseIntError { kind: InvalidDigit })
}
  • |r| r.map(Some)の部分が少し分かりにくいが、rはResult<i32, ParseIntError>型で、エラーがない場合は値をSomeで包んで返す
    • 上の例ではOk(84)が渡されてOk(Some(84))に変換されている(Err(ParseIntErr)の場合はmapされないのでそのまま)
  • 18.4.2. エラー型を定義する
  • 18.4.1では、NoneとParseIntErrorを別々に扱う必要があった
  • これをまとめるために自前のエラー型(DoubleError)を定義していく
use std::error;
use std::fmt;

#[derive(Debug, Clone)]
struct DoubleError;

// エラー情報はシンプルなメッセージのみ
impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "invalid first item to double")
    }
}

// Errorとして扱うためのトレイトを実装する
impl error::Error for DoubleError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        // エラー原因は記録しない
        None
    }
}

// DoubleErrorをもつエイリアスを定義
type Result<T> = std::result::Result<T, DoubleError>;
  • これを利用して配列の最初の要素を2倍にして返す関数を実装すると次のようになる
fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
        // Noneの場合、自前のエラーに変換
        .ok_or(DoubleError)
        .and_then(|s| {
            s.parse::<i32>()
                // Parseエラーを自前のエラーに変換
                .map_err(|_| DoubleError)
                .map(|i| 2 * i)
        })
}
  • ちょっと長くて読みづらいね、と話していたら、一度変数で受けるとand_then()を消せていいかも、という案がでた。
    • ネストも浅くて済むし、良さそう。
fn double_first(vec: Vec<&str>) -> Result<i32> {
    let s = vec.first()
        // Noneの場合、自前のエラーに変換
        .ok_or(DoubleError)?;
    s.parse::<i32>()
        // Parseエラーを自前のエラーに変換
        .map_err(|_| DoubleError)
        .map(|i| 2 * i)
}

 

[まとめ]

モブプログラミングスタイルでRust dojoを開催した。

前回話してた継承で例外を処理するのではなく、新たに例外を定義して処理をまとめていく方法を学んだ。とはいえエラー原因の取り扱いとかもう少し詳しく知りたい...。

 

今週のプルリクエストはこちら。

github.com