kdnakt blog

hello there.

Rust dojo第61回を開催した

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

kdnakt.hatenablog.com

 

 

[第61回の様子]

2022/12/14に第61回を開催した。

内容としてはRust By Example 日本語版の18. エラーハンドリングの「18.4.5. エラーをラップする」〜「18.5. Resultをイテレートする」に取り組んだ。18.5はちらっと見ただけだけど...。

参加者は自分を入れて5人。師走で忙しいけどたくさん集まってよかった。

 

[学んだこと]

  • 18.4.5. エラーをラップする
  • 前回はBox型を使ってエラーをまとめる方法を学んだ
  • しかし、実行時まで型が分からないという問題があったので、今回は具体的なエラーをラップした型を定義していく
  • まず、enumを用いてそれぞれのエラーを定義する
use std::num::ParseIntError;

type Result<T> = std::result::Result<T, DoubleError>;

#[derive(Debug)]
enum DoubleError {
  // ベクターが空のとき
  EmptyVec,
  // ベクターの最初の要素がi32に変換できないとき
  Parse(ParseIntError),
}
  • 次に、DoubleErrorをエラーとして利用できるように、DisplayトレイトとErrorトレイトを実装していく
use std::error;
use std::fmt;

impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            DoubleError::EmptyVec =>
                write!(f, "please use a vector with at least one element"),
            DoubleError::Parse(..) =>
                write!(f, "the provided string could not be parsed as int"),
        }
    }
}

impl error::Error for DoubleError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match *self {
            DoubleError::EmptyVec => None,
            DoubleError::Parse(ref e) => Some(e),
        }
    }
}
  • テキストではfmt()メソッドのコメントに、ラップした元の型の実装に任せると書かれていたが、実際にはラップされた型を利用していない
  • 利用するとしたらこんな感じになりそう
impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            // 省略
            // ref eで元のエラーを参照
            DoubleError::Parse(ref e) => 
                write!(f, "the provided string could not be parsed as int: original error={:?}", e),
        }
    }
}
  • さらに、?演算子でParseIntErrorをDoubleErrorに変換するために、Fromトレイトを実装する
use std::num::ParseIntError;

impl From<ParseIntError> for DoubleError {
    fn from(err: ParseIntError) -> DoubleError {
        DoubleError::Parse(err)
    }
}
  • これを利用して、ベクターの最初の要素を2倍する関数を実装するとこうなる
// Errorトレイトのsource()メソッドを利用するために必要
use std::error::Error as _;

fn double_first(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or(DoubleError::EmptyVec)?;
    // ここで先ほどのFromトレイトの実装を呼び出す
    let parsed = first.parse::<i32>()?;

    Ok(2 * parsed)
}

fn print(result: Result<i32>) {
    match result {
        Ok(n)  => println!("The first doubled is {}", n),
        Err(e) => {
            // Displayトレイトの実装を利用
            println!("Error: {}", e);
            // Errorトレイトの実装を利用
            if let Some(source) = e.source() {
                println!("  Caused by: {}", source);
            }
        },
    }
}

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

    print(double_first(numbers));
    // The first doubled is 84

    print(double_first(empty));
    // Error: please use a vector with at least one element
    
    print(double_first(strings));
    // Error: the provided string could not be parsed as int
    //   Caused by: invalid digit found in string
}
  • sここまでやってみた結果、最後のまとめに書かれていたのが「ボイラープレートが増えて冗長になるのでライブラリを使いましょう、だったのがウケる
  • anyhowとかを使いましょうねってことみたい

zenn.dev

  • 参加したメンバーのひとりが、dojoの後に例外の詳細を持たせる場合の実装を書いてくれた
    • 最終的にwith_message()がanyhowのcontext()みたいになった

play.rust-lang.org

use std::error;
use std::num::ParseIntError;
use std::fmt;

type Result<T> = std::result::Result<T, DoubleError>;

#[derive(Debug)]
enum DoubleError {
    EmptyVec(Option<String>),
    Parse(Option<String>, ParseIntError),
}

impl DoubleError {
    // メッセージ付きでEmptyVecを作成するコンストラクタ
    fn new(message: String) -> DoubleError {
        DoubleError::EmptyVec(Some(message))
    }
}

// 自作のエラー型にメッセージを書き込むユーティリティ
fn add_error_message(e: DoubleError, message: String) -> DoubleError {
    match e {
        DoubleError::EmptyVec(_) => DoubleError::EmptyVec(Some(message)),
        DoubleError::Parse(_, e) => DoubleError::Parse(Some(message), e),
    }
}

impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            DoubleError::EmptyVec(ref message) =>
                write!(f, "DoubleError::EmptyVec: {}", show_optional_message(message)),
            DoubleError::Parse(ref message, ref wrapped_error) =>
                write!(f, "DoubleError::Parse: {}\n  Caused by: {}",
                    // causeがあればそれも再帰的に表示する
                    show_optional_message(message), wrapped_error),
        }
    }
}

// .or_empty_string() みたいなやつ
fn show_optional_message(message: &Option<String>) -> &str {
    message.as_ref().map_or("", |s| s.as_str())
}

impl error::Error for DoubleError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match *self {
            DoubleError::EmptyVec(_) => None,
            DoubleError::Parse(_, ref e) => Some(e),
        }
    }
}

// `ParseIntError`から`DoubleError`への変換の実装。
// `ParseIntError`が`DoubleError`に変換される必要がある時、自動的に`?`から呼び出される。
impl From<ParseIntError> for DoubleError {
    fn from(err: ParseIntError) -> DoubleError {
        DoubleError::Parse(None, err) // デフォルトではエラーメッセージはなし
    }
}

// エイリアス型にメソッドを生やすためのトレイト
trait MessageAppendable<T> {
    fn with_message<F: FnOnce() -> String>(self, message: F) -> Result<T>;
}

// こういう使い方がしたい:
// result.withMessage(|| !format("Some error. param1={}, param2={}, ...", ...))?;
// ? 演算子のfromに頼れなくなるので、よりジェネリックResultに対してwith_messageを定義する
impl<T, U: Into<DoubleError>> MessageAppendable<T> for std::result::Result<T, U> {
    fn with_message<F: FnOnce() -> String>(self, message: F) -> Result<T> {
        self.map_err(|e| add_error_message(e.into(), message()))
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or_else(|| DoubleError::new("please use a vector with at least one element".to_string()))?;
    let parsed = first.parse::<i32>()
        .with_message(|| format!("the provided string could not be parsed as int. value={}", &first))?;
    Ok(2 * parsed)
}

fn main() {
    let strings = vec!["tofu", "93", "18"];
    // ここはVec<Result<i32, std::num::ParseIntError>>と同じ
    let numbers: Vec<_> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Results: {:?}", numbers);
}

// 出力は以下のようにErrかOkが返ってくる
Results: [Err(ParseIntError { kind: InvalidDigit }), Ok(93), Ok(18)]
  • これでは扱いづらいので、Okになった結果だけを集める場合には、filter_map()を利用する
fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<i32> = strings
        .into_iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();
    println!("Results: {:?}", numbers);
}

// 出力は以下の通り
Results: [93, 18]
  • Result.ok()がOption<T>を返すので、それでNoneをフィルタしてくれるらしい。便利。

doc.rust-lang.org

 

[まとめ]

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

最近RustでLeetCodeやってるおかげもあって、最初の頃よりはスムーズに読み書きができるようになってきたと思う。

 

今週はあまり元のコードを変更しなかったのでプルリクエストなし。