LoginSignup
36
22

More than 5 years have passed since last update.

RustでResult<T, E>を返すイテレータに対し、トレイトを作って実装したら頭の体操だった話

Last updated at Posted at 2017-05-14

コメント欄でtermoshttさんに指摘いただいて、余分なSizedを省きました

Rustは厳しい

C++のテンプレートだと、「この型はイテレータだからな!」みたいなオレオレルールを、コンパイラに伝える必要はなく、どうがんばってもコンパイルできないことにコンパイラが気づいたらコンパイルエラーが出る、という、大変ゆるいルールで、事実上ダックタイピングみたいな感じなのですが。
Rustは、そのへんきっちりしてるので、ジェネリックが任意の型だと都合悪い場合には、そのことをコンパイラに伝えないといけないわけです。

分かりやすい例を出すと、以下のコードは

struct Foo<T> { foo: T }
impl<T> Foo<T> {
    fn func(self, other: Foo<T>) -> T { self.foo + other.foo }
}

「Tって足し算できるの? そんなの聞いてない!」ってエラーになります。

error[E0369]: binary operation `+` cannot be applied to type `T`

コンパイラには「この実装は、Tが足し算できるときの実装なんですよ」と教えないといけません。

use std::ops::Add;
struct Foo<T> { foo: T }
impl<T> Foo<T> where T: Add {
    fn func(self, other: Foo<T>) -> T { self.foo + other.foo }
}

じゃあ今度は「え? TとTを足し算したらTになるんだっけ? Add::Output 型になるんじゃなかった?」ってエラーが出ます。
これに対処する方法は2つあって。ひとつは、funcの戻り値の型を Add::Output 型にする方法。

use std::ops::Add;
struct Foo<T> { foo: T }
impl<T> Foo<T> where T: Add {
    fn func(self, other: Foo<T>) -> <T as Add>::Output { self.foo + other.foo }
}

もうひとつは「この実装は、Tが足し算できて、OutputがTの場合の実装なんですよ」とコンパイラに教える方法。

use std::ops::Add;
struct Foo<T> { foo: T }
impl<T> Foo<T> where T: Add<Output=T> {
    fn func(self, other: Foo<T>) -> T { self.foo + other.foo }
}

どちらがいいかは、Fooがどうあってほしいかに依ります。Tが返ってこないと都合が悪い場合は、後者の方がいいでしょうし、Tが返ってこなくても何か返ってきたらいい場合は前者の方がいいでしょう。

トレイトを作ろう

さて。前置きが長くなりましたが。
Resultが返ってくるイテレータ、なる鬱陶しいものがあったので。これを片っ端からunwrap()して返すイテレータと、それを返すトレイトをイテレータに対し実装しようとしました。

やりたいことは、大体、以下のような感じです。
これに、つけなければいけない制約は仕方なくつけながら、コンパイルできるようにしよう、というお話です。

// コンパイルできないよ!
pub trait ResultIter {
    fn unwrap(self) -> Unwrap<Self> {
        Unwrap { iter: self }
    }
}
impl ResultIter for Iterator {}

pub struct Unwrap<I> {
    iter: I,
}
impl<I> Iterator for Unwrap<I> {
    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().map(|e| e.unwrap())
    }
}

苦難のはじまり

まず怒られたのが impl ResultIter for Iterator {}Iteratorってのはダメだ、と。何を返すイテレータか、Itemを示せ、と怒られました。Result<T, E>のイテレータだとコンパイラに教えましょう。

pub trait ResultIter<T, E> {
    fn unwrap(self) -> Unwrap<Self> {
        Unwrap { iter: self }
    }
}
impl<T, E> ResultIter<T, E> for Iterator<Item=Result<T, E>> {}

Sizedとの戦い

続いて怒られたのが、ResultIter<T, E>が、Sizedじゃない、と怒られました。
Sizedは、暗黙のうちに型パラメータにつけられるトレイトで、Unwrap<I>型のIには、暗黙のうちに以下のようにSizedが要求されているので怒られました。

pub struct Unwrap<I: Sized> {
    iter: I,
}

そこで、

pub struct Unwrap<I: ?Sized> {
    iter: I,
}

のように ?Sized にすれば、SizedでもSizedじゃなくてもどちらでもよくなるのですが、
Rustでは、関数の引数と戻り値はSizedである必要があるので、
ResultIter<T, E> の中の fn unwrap(self) -> Unwrap<Self> は、selfUnwrap<Self>もどちらもSizedじゃないとマズいよなーってことがわかります。
仕方ないので、ResultIterUnwrapSelf: Sizedを要求することにします。

pub trait ResultIter<T, E> where Self: Sized {
    fn unwrap(self) -> Unwrap<Self> {
        Unwrap { iter: self }
    }
}
impl<T, E> ResultIter<T, E> for Iterator<Item=Result<T, E>> {}

pub struct Unwrap<I: ?Sized> {
    iter: I,
}
impl<I> Iterator for Unwrap<I> where Self: Sized {
    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().map(|e| e.unwrap())
    }
}

すると、面白いコンパイルエラーが出ました。

error[E0277]: the trait bound `std::iter::Iterator<Item=std::result::Result<T, E>> + 'static: std::marker::Sized` is not satisfied

「いやいや、そもそもIterator<Item=Result<T, E>>って、Sizedじゃないんだけど。それがSizedってどういうことよ?」……とのことです。
Sizedって何?という話はここに詳しく書いてあります。

ならばこれでどうじゃ! (これ思いつくのに2日くらいかかりました。こんなんアリだったんですね)

pub trait ResultIter<T, E>: Sized {
    fn unwrap(self) -> Unwrap<Self> {
        Unwrap { iter: self }
    }
}
impl<I, T, E> ResultIter<T, E> for I where I: Iterator<Item=Result<T, E>> {}

幽霊を呼び出せ

続いて怒られるのは、impl<I> Iterator for Unwrap<I>の方で。「Iteratorの実装するならtype Itemを定義しないといけないんだけど、見当たらない」です。
じゃあtype Item = Tを作れば・・・・って、Tがない!
さらには、IteratorIterator<Item=Result<T, E>>だということも教えとかないと怒られそう。

なければつければいい。
impl<I, T, E> Iterator for Unwrap<I> where Self: Sized

そしたら、もちろん怒られる。「Tってなに? Eってなに? そんなん急に言われても分からん」

error[E0207]: the type parameter `T` is not constrained by the impl trait, self type, or predicates
error[E0207]: the type parameter `E` is not constrained by the impl trait, self type, or predicates

ならばUnwrap型に持たせよう。

pub struct Unwrap<I, T, E> {
    iter: I,
}

怒られました。「T使ってないじゃん! E使ってないじゃん!」

error[E0392]: parameter `T` is never used
error[E0392]: parameter `E` is never used

なので、無理矢理TとEを使わないといけないわけです。
とはいえ、無駄な変数持ちたくないし、そもそもTとEをどう初期化したらいいかも分かりません。
そこで出てくるのが、無駄な型パラメータを消費するためのPhantomDataです。

use std::marker::PhantomData;
pub trait ResultIter<T, E>: Sized {
    fn unwrap(self) -> Unwrap<Self, T, E> {
        Unwrap { iter: self, phantom: PhantomData }
    }
}
impl<I, T, E> ResultIter<T, E> for I where I: Iterator<Item=Result<T, E>> {}

pub struct Unwrap<I, T, E> {
    iter: I,
    phantom: PhantomData<*const (T, E)>,
}
impl<I, T, E> Iterator for Unwrap<I, T, E> where I: Iterator<Item=Result<T, E>> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().map(|e| e.unwrap())
    }
}

PhantomData<*const (T, E)>については、公式ドキュメント内に、PhantomDataの型パラメータを参照やポインタにしなかったら、型が中身を所有しているものとみなされてDrop checkがされるので、所有してないときは参照やポインタにしようね、とあったので、そうしてみました。(正直よく分かってません)

戦いの終わり

まだエラーが出ます。実はResult<T, E>::unwrap()は、EDebugが実装されてないと使えないらしいです。そんなのしらなかった!
どうすりゃいいか、もう簡単ですよね。

use std::fmt::Debug;
use std::marker::PhantomData;
pub trait ResultIter<T, E>: Sized {
    fn unwrap(self) -> Unwrap<Self, T, E> {
        Unwrap { iter: self, phantom: PhantomData }
    }
}
impl<I, T, E> ResultIter<T, E> for I where I: Iterator<Item=Result<T, E>> {}

pub struct Unwrap<I, T, E> {
    iter: I,
    phantom: PhantomData<*const (T, E)>,
}
impl<I, T, E: Debug> Iterator for Unwrap<I, T, E> where I: Iterator<Item=Result<T, E>> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().map(|e| e.unwrap())
    }
}

fn main() {
    let v: Vec<Result<i32, i32>> = vec![Ok(1), Ok(2), Ok(3)];
    assert_eq!(v.into_iter().unwrap().collect::<Vec<_>>(), vec![1, 2, 3])
}

無事、コンパイルが通り、エラーなく実行できました。

さいごに

Rustはコンパイラがきっちりしている、良い言語ですが、あまりにきっちりしすぎていて、コンパイルエラーとの戦いは試練です。(それでも多くの場合、きれいな解決法があるので、パズルの解き甲斐はあります)
まだ新しい言語で、特に日本語では情報が少ないので、多くの情報を書き散らかしながら、ググればHOWTOがすぐに見つかる状態にしたいなぁ、という気持ちもあります。

実は今回のトレイトの、もう少しいろんな機能つけたのを、GitHubCrates.ioで公開しています。
が、最初に書いたコードは、それはそれは、ひどいものでした。
いろいろと戦って、
pub trait ResultIter: Iterator where Self: Sized, <Self as Iterator>::Item: Result<T, E>
とすると、「トレイトがくるはずのところにEnumがきてる!おかしい!」と怒られ、苦し紛れに無理矢理トレイトを作ったら、闇が闇を呼ぶコードになりました。

なんとかならんのか、ともう一度考えてみたら、かなりきれいにパズルがとけて、嬉しくなって記事を書いてみた次第です。
嬉しい、楽しいはエンジニアの原動力の一つなので。そういう純粋な喜びから、良いプログラムや良い記事がいっぱい書けたらいいなぁ、と思う次第です。

36
22
3

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
36
22