イテレータによるオプション/結果の転置


この記事では、私が遭遇した問題についての私の考えを共有したいと思います.私は確かに問題と私のソリューションで作った決定の両方のコメントや意見を聞いてみたい.
この記事のトピックは、オプションのイテレータにオプションのコレクションまたはイテレータを変換するか、「移す」ことです.よく知っているかもしれない Option::transpose and Result::transpose メソッド変換Option<Result<T, E>> into Result<Option<T>, E> と逆も同様です.私の目標は非常に似ていますが、代わりに、私は変換したいと思いますOption<I: IntoIterator> into impl Iterator<Option<I::Item>> and Result<I: IntoIterator, E> into impl Iterator<Result<I:Item, E>> .
まず、その背後にあるモチベーションとそのような操作の定義について議論します.それから、私はこの機能の実装を通してあなたを歩かせますOption and Result を追加します.うまくいけば、モチベーションがあなたを納得させることができなくても、実装の詳細にはまだいくつかの値があります.
キーワードrust algorithm iterator
ソースコード
私は、この記事からコードを共有しましたGithub . 私もアップロードreference documentation 閉じるこの動画はお気に入りから削除されています.

動機
まず第一に、私はあなたと共有しましょう.私は、この問題を第一に見たいと思いました.つの値を格納する型を持っていると仮定します.後者はオブジェクトを作成するときに提供される必要はありません、そして、それがなくなっているならば、それは計算されるか、デフォルト値を与えられます.
struct Entry {
    id: String,
    value: u64,
}

impl Entry {
    pub fn new(id: String, value: Option<u64>) -> Self {
        Self {
            id,
            value: value.unwrap_or_else(Self::default_value),
        }
    }
    fn default_value() -> u64 {
        todo!("Compute or provide default")
    }
}
IDを行単位のファイルから読み込みます.値はいくつかの他のプロセスによって計算され、またファイルに格納されます.私たちの主なプログラムは、最初のファイルとオプションで値を1つかかります.この特定の設定に関する詳細は重要ではありません.ポイントは、すべての値またはnoneを持つことです.このデータをロードしますVec<Entry> . 以下に例を挙げることができます(簡単にするためにエラーを処理するのではなく、パニックを起こすことに注意してください.
fn load_entries<R: BufRead>(ids: R, values: Option<R>) -> Vec<Entry> {
    let mut values = values.map(|r| {
        r.lines().map(|line| {
            line.expect("cannot read line")
                .parse::<u64>()
                .expect("cannot parse as u64")
        })
    });
    ids.lines()
        .map(|line| {
            let id = line.expect("cannot read line");
            Entry::new(id, values.as_mut().and_then(|values| values.next()))
        })
        .collect()
}
これは確かに動作しますが、少し冗長なので、この文脈では変更可能なイテレータが嫌いです.私は、ちょうど私が2人をつなぐことができたことを願います.そして、これは正確に私が以下のように設定したものです.

イテレータアダプタ
イテレータアダプターは std::iter::from_fn 下記をご覧ください.
fn transpose_into_iter<I: IntoIterator>(iter: Option<I>)
    -> impl Iterator<Item = Option<I::Item>>
{
    let mut inner = iter.map(|i| i.into_iter());
    std::iter::from_fn(move || {
        inner.as_mut().map_or(Some(None), |iter| iter.next().map(Some))
    })
}
それはかなり簡単だった.我々は、単に我々が前にしたものをカプセル化しますmap 関数.すぐに機能全体を通過しましょう.引数は型ですOption<I> どこI 実装 IntoIterator . これは本質的に我々が呼ぶことができることを意味するinto_iter() 型の任意の値I 要素の反復子を取得するには注意IntoIterator 何かのために些細に実行されるI: Iterator , その場合into_iter() 単にself . その他多くの実装IntoIterator , 例えばコレクションなどVec and HashSet , スライス、さらにはOption and Result .IntoIterator 二つあるassociated types : Item and IntoIter . 後者は型反復子であり、前者はイテレータの要素の型です.これらの型をダブルコロンで参照できます.I::Item . 関数の戻り値の型を定義する際にこれを利用します.impl Iterator<Item = Option<I::Item>> . この関数は、Iterator 形質とその要素の型Option<I::Item> .言い換えれば、結果として生じるイテレータの要素は、入力イテレータ(そのような存在ならば)で包まれる要素ですOption .
関数の本体では、まずOption<I: IntoIterator> to Option<I: Iterator> . その後、我々は特別なを返しますfrom_fn イテレータアダプタ.単純にイテレータのたびに渡されたクロージャを呼び出しますnext() メソッドが呼び出されます.常に戻るSome(None) (したがって、None ) if inner is None . どのようにこれが無限イテレータをもたらすかについて注意してくださいNone . If inner is Some(_) , 次の要素をラップしますSome 基になるイテレータが要素からなくなるまで.コードへのマイナーな変更は、それを生産するNone すべての既存の要素の後.我々が呼ぶことができるように、後者がより一般的である方法に注意してくださいtake_while(Option::is_some) これを有限系列に減らす.しかし、私は最初のアプローチに固執することを決めた.どちらかに賛成の意見があれば知らせてください.
いずれにしても、このアダプタをこの例で使うことができます.
fn load_entries_with_transpose<R: BufRead>(ids: R, values: Option<R>)
    -> Vec<Entry>
{
    let values = values.map(|r| {
        r.lines().map(|line| {
            line.expect("cannot read line")
                .parse::<u64>()
                .expect("cannot parse as u64")
        })
    });
    ids.lines()
        .zip(transpose_into_iter(values))
        .map(|(line, value)| Entry::new(line.expect("cannot read line"), value))
        .collect()
}
ニース、これ以上のmutable状態.私たちはそれをそのまま残すことができますし、一日と呼ばれる可能性がありますが、我々は別の機能を持っていることに注意してくださいResult 我々は単に別のタイプのためにそれを過負荷することはできません.さびにおいて、機能過負荷は特徴によって達成される.しかしimpl Trait 戻り値は引数関数では許可されませんので、具体的な(関連付けられた)型を考え出す必要があります.

形質の実装
前に述べたように、我々は特徴の中で具体的なタイプを返さなければならない.
pub trait IterTranspose {
    fn transpose_into_iter(self) -> T;
}
さて、何をすべきかT そうですか.それはおそらくどのようなタイプが特徴を実装するかによって異なります.これは関連する型には最適です.
pub trait IterTranspose {
    type Iter: Iterator;
    fn transpose_into_iter(self) -> Self::Iter;
}
現在、戻り値は具体的な型を持ちますが、関連する型であるため、実装に依存します.何がIter 上の例は?閉じるこの動画はお気に入りから削除されています.我々はそれを設定できましたstd::iter::FromFn<Box<FnMut() -> Option<T>>> しかし、我々はさらに定義する必要がありますT そして、もっと重要なことに、それはダイナミックディスパッチを導入するでしょう.代わりに、我々は単純なを作成することができますstruct その意味Iterator 直接、これは以前の実装と同じくらい簡単です.
pub struct OptionTransposedIter<I> {
    inner: Option<I>,
}

impl<I: Iterator> Iterator for OptionTransposedIter<I> {
    type Item = Option<I::Item>;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner
            .as_mut()
            .map_or(Some(None), |iter| iter.next().map(Some))
    }
}
現在、我々は実装する準備ができていますIterTranspose :
impl<I: IntoIterator> IterTranspose for Option<I> {
    type Iter = OptionTransposedIter<I::IntoIter>;

    fn transpose_into_iter(self) -> Self::Iter {
        OptionTransposedIter {
            inner: self.map(I::into_iter),
        }
    }
}
そして最後に、この例は新しいインターフェースを使用しているようです.
fn load_entries_with_transpose<R: BufRead>(ids: R, values: Option<R>)
    -> Vec<Entry>
{
    use IterTranspose;
    let values = values.map(|r| {
        r.lines().map(|line| {
            line.expect("cannot read line")
                .parse::<u64>()
                .expect("cannot parse as u64")
        })
    });
    ids.lines()
        .zip(values.transpose_into_iter())
        .map(|(line, value)| Entry::new(line.expect("cannot read line"), value))
        .collect()
}

結果
の実装Result<T, E> 非常に類似しています、そして、私はここでそれを繰り返しません、しかし、あなたが私の実装に興味があるならば、訪問してくださいGithub repository . つの重要な違いはErr variant型の値を保持するE そして繰り返し生産する必要がある.これは実装しなければならないことを意味しますE: Clone . それに加えて、Result 上記のものと似ています.

概要
我々は、どのようにOption<I: IntoIter> (or Result<I: IntoIter, E> ) にimpl Iterator<Item = I::Item> , どのように Option::transpose 変換するOption<Result<T, E>> into Result<Option<T>, E> . このような操作は、たとえば、コレクションを任意の別のコレクションでzip化するときに便利です.我々はまず、この問題を解決する方法を見た std::iter::from_fn , 次に、複数の型のアダプター関数をオーバーロードする方法Option and Result ) カスタム型を関連付けられた型で使用する.
我々が解決した問題はむしろニッチであるけれども、私は解決への道が面白いとわかりました.イテレータ、特性、一般的なプログラミングなどの錆のいくつかの重要な部分に触れる.また、私は今まで以上に遭遇している問題に対処し、一般的な解決策としてアプローチし、それを木枠として書くのは楽しい.
あなたが何か質問や提案がある場合は、上または上に到達することを躊躇しないでくださいMastodon .