60

この記事は最終更新日から3年以上が経過しています。

投稿日

更新日

Rust における `From<T>` とか `Into<T>` とかの考え方

Rustを始めてみて。他の言語とは感覚が違うなあ、と感じる点は、ある型からある型への値の明示的な変換、というものを意識する機会がけっこう多いところ。

たとえば、関数の引数や戻り値のシグネチャが、 From<T>Into<T>AsRef<T>Fromなんちゃら、といった名前のTrait制約を要求してくることが割に多い。これらのTraitは、「どんな振舞いをする値か?」という情報というよりはむしろ、「どんな値に変換できるのか?」といったことを言っている。

ボクシングを避ける

GC つきの多くの言語やc++なんかでは、異なる具象型をもつ値を同じ変数として取り扱いたいとき、ヒープ領域を確保して値を隔離して、値そのもののかわりに「参照の値」を変数にいれておく、という方法がしばしば使われる。
参照(ポインタ)それ自体は平等に同じサイズの値として取り扱うことができるため、同じ変数にいれたい場面でもことなきをえる、という仕組み。
これは「値のボクシング」とか呼ばれ、OOPパラダイムを搭載する大抵の言語では当たり前のように採用される。

ところが、Rust では、原則、そういうことはしない。

ボクシングをしないことで、以下のようなデメリットを取り除こうとしているのが、Rustの実用的な道具としての姿になっている。

  • ヒープアロケーションをしなくて済む
    • ヒープに確保したメモリをきちんと解放する管理コストが発生しない
    • GCが必要ない
    • メモリ管理のためのパフォーマンス上のオーバヘッドが発生しない
  • 呼び出す関数と、関数の引数と戻り値の型が コンパイル時に確定する
    • 動的ディスパッチのコストがまったくないため速い
    • 実行時の型の変換がなければ、意図しない動作をする余地がより少なくなる

ボクシングのかわりに提供されている道具のうちのひとつが、From<T>Into<T> 。ということになるのだろうか。
異なる型の値を抽象的な値としてラップしてしまうのではなく、値から値への互換性を明示的に準備しておく、という方法が採られている。

(もちろん、必要な場面では、Rustでもボクシングをすることはできる。しかし、僕達が知らないうちにランタイムが自動的にボクシングをする、ということはない)

実行時ではなく、コンパイル時に変換が確定する

ボクシングをしない方針でいくと、呼び出す関数の実装は、実行時でなくコンパイル時に完全に確定している必要がでてくる。このことが書き方にも影響してくる。

よく、こういう見た目のRustのコードがある。

let a: Vec<_> = iter.collect();

なぜ型アノテーションが左辺に必要なのか、他の言語だとあまりみかけない見た目ではないだろうか。
ためしに、型アノテーションを取り除くと、このコードはコンパイルできない。

let a = iter.collect();

// コンパイルエラー
//     let a = iter.collect();
//  |         ^
//  |         |
//  |         cannot infer type
//  |         consider giving `a` a type

.collect() は、 FromIterator<T> というtraitの関数だけど、この T が何になるかというのは、戻り値によって変わる。

このように、レシーバは1つだけど、ただし返し得る型のバリエーションは複数、といった関数を呼ぶとき、Rustは「コンパイル時に具体的になんの値になるかいつも確定させる」ということを書き手に要求する。(「どんな値になるかわからない抽象オブジェクトを返す」ということはしない) だから、どの .collect() を使うのか、手がかりがない場合は型アノテーションを書いてあげる、ということになる。

似ているけど型は違うものを同じ関数に渡したいときなども、 .into() で明示的に変換してから渡したり、 関数の引数を From<変換元> にするとか、そういうコードを書くことになる。

let addr = ([0, 0, 0, 0], 3000).into(); // コンパイルエラー

Server::bind(([0, 0, 0, 0], 3000).into()); // ok
Server::bind("0.0.0.0:3000".into()); // ok

( 上のような場面で、現状のRustのIDEでは .into() の実装に一発でジャンプできなくてちょっと不便……

impl Trait

Rust は、「実行時にどの値になるかわからないオブジェクトを返す」ということは(明示的にボクシングしない限りは) 許されていない。
そのため、こういうことはできない。

trait Hoge {
}

// Traitを具体的な型として扱うことはできない
fn hoge() -> Hoge {
    // ...
}

Hoge が実装された具体的な値が何か、決まっていないといけない。

それから、以下もコンパイルエラーになる。

trait Hoge {
}

struct A {
}

impl Hoge for A {
}

fn hoge<H: Hoge>() -> H {
    A {}
// |     ^^^^^^^^ expected type parameter, found struct `AHoge`
}

A という具象型を、 Hoge として扱う、ということが不可能だからだ。

この場合、impl Trait という特別な書き方をすることで意図した動作をさせることができる。

fn hoge() -> impl Hoge {
    A {}
}

これも、Hoge という型を返しているわけではなくて、コンパイル時にどのHogeが採用されるかが解決される、という特別な機能になっている。だからこそ、 -> Hoge ではなく impl Hoge という、ちょっと特別な戻り値であることを書き手は明示させられる。

また、この例の場合、以下のように、コンパイル時に選択される実装に基いて型も決まる書き方をすると問題なくコンパイルできる。

trait Hoge {
    fn hoge() -> Self;
}

struct A {
}

impl Hoge for A {
    fn hoge() -> Self {
        A {}
    }
}

fn hoge<H: Hoge>() -> H {
    H::hoge()
}

Monomolization

そんなこんなで、rustでは、値から値へ変換するtraitを使う場面がよくでてくる。

ところで、ボクシングすることのメリットとして、実行時のパフォーマンスの他に、コンパイラが生成するジェネリクスのコードのバリエーションを抑えることができる、というものもある。

これについては、ジェネリクスの型パラメータに From<T> などをつかうことで、似たようなメリットを得ることができるようだ。

公式Bookで言われているように、基本的には、Rustは、ジェネリクスの型パラメータの数だけ、実装を生成するらしいですが、

下の例のように、ジェネリクスの型パラメータが Into<i32> などの、「i32に変換できる型」として定義しておけば、コンパイラは、それぞれのジェネリクスで異なっているのが 「変換方法のみ」であると判断できるため、コンパイラが生成しなければいけないコードが節約できるらしい。

pub fn big_function<T: Into<i32>>>(x: T) {
    // This is a giant function with hundreds of lines!
    // And it gets called with a lot of concrete types!
    // Oh no!
}
pub fn big_function<T: Into<i32>>(x: T) {
    let x: i32 = x.into();
    _big_function(x)
}

fn _big_function(x: i32) {
    // This is where all the rest of the original function body is now!
}

参考: Monomorphization Bloat - Suspect Semantics

いろいろなTrait

From<T>

From<T> は、 ある値からある値への 変換方法を定義する基本的なtrait。よく使う。

impl From<変換元> for 変換先 {
    fn from(from: 変換元) -> 変換先 {
        // ...
    }
}

エラー伝搬 (Propagation)

? をつかった Rust のエラー伝搬機能は、この From<T> が自動で呼びだされることで実現されてる。

fn hoge() -> Result<(), HogeError> {
    File::open("...")?;
}

この場合、File::open が返すかもしれない、 io::Error と、この関数自体のエラー表現である HogeError は別々の型なので、 このままだとコンパイルエラーになってしまう。

だけど、From<io::Error> を HogeError に実装してあげることで、 コンパイラは必要な場合に自動的に io::Error から HogeError への変換を呼びだしてくれるようになる。

impl From<io::Error> for HogeError {
    // ...
}

また、Future における エラーの変換、.from_err() でも From が自動的に呼ばれるようになっている。

Into<T>

Into<T> は、From<T> の逆。

Into<変換先>

Into<T> は、自分で実装する必要はない。From<T> を実装しておけば、自動で逆の変換も実装される。

struct A {}
struct B {}

impl From<B> for A {
    fn from(from: B) -> A {
        A {}
    }
}

fn main() {
    let b = B {};
    let a = A::from(b);

    let b = B {};
    let a: A = b.into();
}

AsRef<T> / AsMut<T>

AsRef<T> は、「Tへの参照への変換」が実装されるTrait。

pub trait AsRef<T: ?Sized> {
    /// Performs the conversion.
    #[stable(feature = "rust1", since = "1.0.0")]
    fn as_ref(&self) -> &T;
}

スマートポインタのような型を、渡したい場合に、スマートポインタそれ自身が必要なのではなくて必要なのは中身の参照、ということがよくあるので、そういうときに便利そう。

ミュータブルな参照を返せるバージョンの AsMut<T> もある。

ToOwned<T>

Rust は、よく宣伝されているように、変数のライフタイムが厳格に定められているので、スコープが限定されている変数への参照と、そうではない値とを区別してあげることになる。

たとえば、&str は 特定の文字列への参照なので、ライフタイムを越えて外へ持ち出すことはできないけど、 所有権を持った String として扱うと、値として他の場所へ渡したり、Cloneしたりできるようになる。
この &str を String にするといった関係を ToOwned は表現している。

ToOwned<T> は、ある参照を、所有権を持った値へ変換、to_owend() という関数を実装するTrait。

let str = s.to_owned(); // Stringを返す

Cow<'a, >

Cow は 「Clone-on-write」の略で、スマートポインタの一種。
これも、ある値を透過的にある値として扱いたい場合に利用されることがあるので紹介。

Cow は、&strString のような、「owned」されているかどうかだけの違いで、実際のところ表現している値が同じものについて、どちらかを入れておくことができる enum。

関数の引数を、Into<Cow<'a, str>> などにしておくと、&str でも String でも受けとることのできる関数が書けて便利。

そんなかんじです

この辺に慣れているか慣れていないかで、最初の読み書きのスピードが変わってくるのではないかと感ずる :thinking:

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について
60