🚶

足を止めて見る #3 〜 RustのTryFromトレイト 〜

に公開

足を止めて見よう

足を止めて見ようシリーズの3つ目です。

前回は From トレイトについてでした。

std::convert::TryFrom トレイトを実装しよう

ある型に対して、別の型から変換してその型を作る方法を定義できるのが std::convert::From トレイトでしたね(復習)

変換に失敗するケースが存在する場合に利用するのが std::convert::TryFrom トレイトです。

今回は足を止めて std::convert::TryFrom トレイトを実装してみます。

std::convert::TryFrom トレイトを実装する構造体を用意してみる

下準備として、Person という構造体を用意してみます。
構造体には、nickname と age フィールドを持っています。
ageフィールドは、プリミティブ型ではなくAge型を取るようにしてみます。

#[derive(Debug)]
pub struct Person {
    nickname: String,
    age: Age,
}

#[derive(Debug)]
struct Age(u8);

fn main() {
    let age_value = 35_u8;
    let nickname_value = "yosshi-";
    let yoshida = Person {
        nickname: String::from(nickname_value),
        age: Age::try_from(age_value), // try_from を呼び出してみます
    };
    assert_eq!(yoshida.nickname, nickname_value);
    assert_eq!(yoshida.age.0, age_value);

    println!("実行は正常に終了しました");
}

try_from メソッドを使ってみたいので Age::try_from(age_value) と書いてみました。

無理やり実行してみると、いくつかのエラーになってしまいます(当然ですね)。

the trait bound 'Age: TryFrom<u8>' is not satisfied
required for 'u8' to implement 'Into<Age>'
required for 'Age' to implement 'TryFrom<u8>'

expected struct 'Age'
  found enum 'Result<Age, Infallible>'

1つ目は 'u8' to implement 'Into<Age>''Age' to implement 'TryFrom<u8>' から、実装が足りていないですと言われていますね。まだ実装していないので当然ですね。

2つ目は、age には Age型 が来て欲しいのだけど、Result<Age, Infallible> が受け取れてしまいました、と言われています。それもそのはずで、try_fromResult を返すことで、型変換が失敗する可能性を表しているからですね。

ただ、まだ impl TryFrom<u8> for Age を書いていないのですが、どうして try_from メソッドを呼び出すところまではいけているんでしょう?

これは prelude によるものです。
以下の順序を辿って呼び出すことができるようになっています。

  1. prelude により use std::convert::{Into, From, TryFrom} が自動でインポートされる
  2. impl From<Age> for Age が自動的に実装される(参考: convert/mod.rs:774
  3. impl Into<Age> for Age が自動的に実装される(参考: convert/mod.rs:757
  4. impl TryFrom<Age> for Age が自動的に実装される(参考: convert/mod.rs:813

単純に「use std::convert::* を自動でやってくれているもの」という認識だと、僕のようなつまずき方をしてしまうのではないでしょうか。

こうなると、prelude によって何が自動的に impl されるのかは、ある程度把握しておいた方が良さそうですね(頑張って足を止めて見よう...)

https://doc.rust-lang.org/std/prelude/index.html

std::convert::TryFrom トレイトを実装してみる

rust-analyzerの力を借りながら、std::convert::TryFrom トレイトを実装し、try_from メソッドを書いていきます。

#[derive(Debug)]
pub struct Person {
    nickname: String,
    age: Age,
}

#[derive(Debug)]
struct Age(u8);

// POINT1: あり得ない(0〜130に収まらない)年齢だった場合のエラーを定義
#[derive(Debug)]
enum AgeError {
    Impossible,
}

// POINT2: TryFromの実装を追加する
impl TryFrom<u8> for Age {
    type Error = AgeError;

    // POINT3: u8なので0以上は保証されている
    // POINT4: 世界最高齢は122歳なので、130超はあり得ないことにしている
    fn try_from(value: u8) -> Result<Self, Self::Error> {
        if value > 130 {
            return Err(AgeError::Impossible);
        }
        Ok(Age(value))
    }
}

fn main() {
    let age_value = 135_u8;
    let nickname_value = "yosshi-";
    let yoshida = Person {
        nickname: String::from(nickname_value),
        // POINT5: Result<Age, Infallible> から無理やり Age を取り出してみる
        age: Age::try_from(age_value).expect("年齢の変換は成功するはず"),
    };
    assert_eq!(yoshida.nickname, nickname_value);
    assert_eq!(yoshida.age.0, age_value);

    println!("実行は正常に終了しました");
}

なんとか書いてみました。年齢ということで 0歳未満 または 130歳超 はあり得ないということにしました。

impl TryFrom<u8> for Age { の次の行に type Error = (省略) という見慣れないものが登場しています。

これは関連型というやつですが、今回は無視してしまいます。

Age::try_from(age_value)Result を返します。無理やり中身の Age型 を取り出すために .expect("年齢の変換は成功するはず") としてみました。

こうして実行してみると、なんとか成功しました。

もう一段だけ深ぼってみる #1

最初エラーになったメッセージの中で Infallible というあまり見かけないエラー型が登場しました。これは何だったのでしょうか?

expected struct 'Age'
  found enum 'Result<Age, Infallible>'

Infallible を辞書でひいてみると 誤りのない とか 絶対に正しい といった意味のようです。おそらく エラーになることがない エラーみたいな立ち位置なのではないでしょうか。

公式をみると、どうやら Result が必ず Ok を返す時に指定する慣習?があるみたいですね。

https://doc.rust-lang.org/std/convert/enum.Infallible.html

またRustにはnever型と呼ばれる ! という名前の特別な型がありますが、これと同じ役割と記載があります。

pub type Infallible = !;

Infallible もゆくゆくは ! に統合されてaliasとなっていく予定があるようです。

振り返り

今回も結局 core/convert/mod.rs まで読みにいってしまいました。徐々にですが「これはどんな条件で自動実装されるのか?」みたいな視点で、impl節をパラパラと見て回るような動きができてきた?ので、すこーしだけ成長を感じます。

これで明日から、もっと堂々と try_from を使っていけるぞー 🙌

その他

今回書いたRustのコードはこのリポジトリで制作しています。

https://github.com/kenshuhori/rust/tree/main/workspace/dive_3_try_from_trait

GitHubで編集を提案
ドクターメイト

Discussion