1
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Rust 100 Ex 🏃【22/37】 コンビネータ・RPIT ~ 🐉 「 `Iterator` トレイトを実装してるやつ」~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第22回になります!

今回の関連ページ

[06_ticket_management/07_combinators] Combinators イテレータのコンボ技!

問題はこちらです。

lib.rs
// TODO: Implement the `to_dos` method. It must return a `Vec` of references to the tickets
//  in `TicketStore` with status set to `Status::ToDo`.
use ticket_fields::{TicketDescription, TicketTitle};

#[derive(Clone)]
pub struct TicketStore {
    tickets: Vec<Ticket>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Ticket {
    pub title: TicketTitle,
    pub description: TicketDescription,
    pub status: Status,
}

#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Status {
    ToDo,
    InProgress,
    Done,
}

impl TicketStore {
    pub fn new() -> Self {
        Self {
            tickets: Vec::new(),
        }
    }

    pub fn add_ticket(&mut self, ticket: Ticket) {
        self.tickets.push(ticket);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ticket_fields::test_helpers::{ticket_description, ticket_title};

    #[test]
    fn todos() {
        let mut store = TicketStore::new();

        let todo = Ticket {
            title: ticket_title(),
            description: ticket_description(),
            status: Status::ToDo,
        };
        store.add_ticket(todo.clone());

        let ticket = Ticket {
            title: ticket_title(),
            description: ticket_description(),
            status: Status::InProgress,
        };
        store.add_ticket(ticket);

        let todos: Vec<&Ticket> = store.to_dos();
        assert_eq!(todos.len(), 1);
        assert_eq!(todos[0], &todo);
    }
}

ステータスが ToDo になっているチケットだけからなる動的配列を返すメソッド to_dos を作ってほしいという問題です。

解説

TypeScriptなんかにもある mapfilter 等の高階関数の回です! for 文を使わないでなるべく高階関数でなんでも済ませようって風潮、どの言語にもありますよね(?)

lib.rs
impl TicketStore {
    pub fn to_dos(&self) -> Vec<&Ticket> {
        self.tickets
            .iter()
            .filter(|ticket| ticket.status == Status::ToDo)
            .collect()
    }
}

filter はともかく他言語話者的には見慣れないメソッドがいくつか見られる気がするので軽く解説。

  • iter: 前回内容になりますね、 tickets の中身を不変参照でイテレートしてくれるイテレータを返します。
  • filter: 他言語にも馴染みがあるであろう、返り値が真になる要素のみ残すイテレータのメソッドです。
    • |ticket| ...クロージャと呼ばれる文法です!JavaScript/TypeScriptで言うアロー関数、Pythonで言うラムダ式に当たるものです。
    • 真偽値の代わりに Option 型で Some(T) になるものだけ残す filter_map も何かと便利です。
  • collect: 見かけないやつですね。後述

この collect というのは、イテレータ( filter の返り値もまたイテレータです)を動的配列(等)1に変換するメソッドです。

collect に関連する最大の注意点、あるいはRustのイテレータ高階関数の特徴になりますが、 filter が呼ばれた時点では 繰り返しは実行されていません 。要は 遅延実行されます

collect が呼ばれると、イテレータが具体的な値を持つ動的配列に変換される過程で、 実際に繰り返し実行されます 。遅延実行のお陰で、「〇〇して、△△して、□□した某」という感じで高階関数を重ねることができ、かつそれが一重ループで済むのでとても効率的です!

遅延評価はRustのゼロコスト抽象化と呼ばれる特徴の一端を担っている機能だと思います。そして、関数型言語でよく見られる特徴ですね。最近Elixirの本を少し読んでいるのですが、 Stream がそれに該当していたはずです。この後の章で取り扱う非同期処理も遅延評価だったりします。非同期処理も(tokio ランタイムが持つ特殊なメソッドを使用しない限り)、 .await が呼ばれるまでタスクは一切実行されません!

[06_ticket_management/08_impl_trait] 遅延評価の利用とRPIT

問題指示は以下です。

  • in_progress メソッドを実装しましょう!
  • このメソッドはステータスが InProgress 状態のチケットを浚う(という表現で良いのかな?)イテレータを返します

先の問題で「遅延評価されるお陰でイテレータをゼロコストでチェインさせられてYeah!」みたいなことを言ったと思うのですが、「よし、じゃあ連結させやすいように"イテレータ"を返すようにしよう!」という問題です。

テストを含めた全体
lib.rs
// TODO: Implement the `in_progress` method. It must return an iterator over the tickets in
//  `TicketStore` with status set to `Status::InProgress`.
use ticket_fields::{TicketDescription, TicketTitle};

#[derive(Clone)]
pub struct TicketStore {
    tickets: Vec<Ticket>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Ticket {
    pub title: TicketTitle,
    pub description: TicketDescription,
    pub status: Status,
}

#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Status {
    ToDo,
    InProgress,
    Done,
}

impl TicketStore {
    pub fn new() -> Self {
        Self {
            tickets: Vec::new(),
        }
    }

    pub fn add_ticket(&mut self, ticket: Ticket) {
        self.tickets.push(ticket);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ticket_fields::test_helpers::{ticket_description, ticket_title};

    #[test]
    fn in_progress() {
        let mut store = TicketStore::new();

        let todo = Ticket {
            title: ticket_title(),
            description: ticket_description(),
            status: Status::ToDo,
        };
        store.add_ticket(todo);

        let in_progress = Ticket {
            title: ticket_title(),
            description: ticket_description(),
            status: Status::InProgress,
        };
        store.add_ticket(in_progress.clone());

        let in_progress_tickets: Vec<&Ticket> = store.in_progress().collect();
        assert_eq!(in_progress_tickets.len(), 1);
        assert_eq!(in_progress_tickets[0], &in_progress);
    }
}

解説

先ほどのコードと似たように書き、 collect を消せばよいところまではわかるでしょう。しかし 返り値の型をどう指定すればよいか というところで詰まるかもしれません。まさに今回のエクササイズのメイントピックになります!

とりあえず、 filter メソッドの返り値型になりそうということで、意気揚々と型を書いてみますが...

lib.rs
impl TicketStore {
    pub fn in_progress(&self) -> std::iter::Filter<?, ?> {
        self.tickets
            .iter()
            .filter(|ticket| ticket.status == Status::InProgress)
    }
}

? で表したところで記述に詰まってしまうと思います。1つ目には Iterator トレイトを実装した某、そして2つ目にはクロージャを表す FnMut(&Self::Item) -> bool トレイト(型ではなくトレイトです!)を実装した某を指定しなければなりません。

...この ?具体的な型名を得るのはとても困難 です。特にクロージャの方の型はコンパイラしか知りえません2

解決策として、「顧客が本当に返してほしい型、もとい、機能」に注目してみます。今回欲しいのは イテレータ です。イテレータは特定の型ではなく、トレイト Iterator を実装した型のことを言うのでした。

最終的にはこんな風に書きたいということになります。

Rust
impl TicketStore {
    pub fn in_progress(&self) -> &Ticketを浚うイテレータを実装しているやつ {
        self.tickets
            .iter()
            .filter(|ticket| ticket.status == Status::InProgress)
    }
}

「〽正式名称が~わからないタイプも~好き好き大好き~」3というわけで、正式名称はわからないですが、とにかく「Iterator<Item = &Ticket> トレイト(&Ticket型アイテムを浚うイテレータを表すトレイト)を実装した型」が返り値型であることはわかっています。

そして、 どうせ Iterator トレイトが提供するメソッドしか使いません 。本当の型である std::iter::Filter<...> であるかどうかには関心がないのです。

そういう時に使えるのが RPIT (Return Position Impl Trait) です!さっきの好きな型発表ドラゴン的な書き方をそのまま実現した文法です!(演習の答えになります)

lib.rs
impl TicketStore {
    pub fn in_progress(&self) -> impl Iterator<Item = &Ticket> {
        self.tickets
            .iter()
            .filter(|ticket| ticket.status == Status::InProgress)
    }
}

これで in_progress メソッドの返り値は Iterator<Item = &Ticket> なやつであることを呼び出し元に伝えることができます!RPITを使用することで、面倒な型パズルをパスすることができるのです!

注意点として、RPITにおける impl Iterator<Item = &Ticket> はあくまでも「 &Ticket を浚うイテレータを実装している 正式名称がわからない『特定の』 タイプ」であることに留意してください。

何が言いたいかというと、RPITはコンパイル時に 型自体は一つに決定 されるということです。次のエクササイズで今度は引数位置にある impl Trait を扱い、こちらは関数のジェネリクスと同様4にトレイト境界を満たす型ならなんでも受け取ることができますが、一方でRPITでは「(例えば条件分岐を駆使して) 複数種類の impl TraitTrait を満たす型を返しうる」ようにはできません!

ちょっと言葉だとわかりにくいので...例えばこれはコンパイルエラーです!(エクササイズに合わせてイテレータの問題にしたらちょっと複雑になってしまった...)

Rust (コンパイルエラー!)
fn specific_company_people(
    people: &[Person],
    company: Option<Company>
) -> impl Iterator<Item = Person> + '_ {
    match company {
        Some(company) => {
            people.iter()
                .cloned()
                .filter(move |p| p.company.clone() == company)
        },
        None => people.iter().cloned()
    }
}
例全体

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d92682551dbdb3185095808d55e91d7b

Rust
#[allow(unused)]
#[derive(Debug, Clone, PartialEq, Eq)]
enum Company {
    Yumemi,
    Nemumi,
    Samumi,
    Other(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct Person {
    name: String,
    company: Company,
}

// Ok
fn yumemi_people(people: &[Person]) -> impl Iterator<Item = Person> + '_ {
    people.iter()
        .cloned()
        .filter(|p| p.company.clone() == Company::Yumemi)
}

// ダメ!
fn specific_company_people(
    people: &[Person],
    company: Option<Company>
) -> impl Iterator<Item = Person> + '_ {
    match company {
        Some(company) => {
            people.iter()
                .cloned()
                .filter(move |p| p.company.clone() == company)
        },
        None => people.iter().cloned()
    }
}

fn main() {
    let people = [
        Person {
            name: "ゆめ太郎".to_string(),
            company: Company::Nemumi,
        },
        Person {
            name: "やめ太郎".to_string(),
            company: Company::Yumemi,
        },
    ];
    
    let mut y_people = yumemi_people(&people);
    
    assert_eq!(&people[1], &y_people.next().unwrap());
    
    let mut n_people = specific_company_people(&people, Some(Company::Nemumi));

    assert_eq!(&people[0], &n_people.next().unwrap());
}
コンパイルエラー
   Compiling playground v0.0.1 (/playground)
error[E0308]: `match` arms have incompatible types
  --> src/main.rs:31:17
   |
25 |       match company {
   |       ------------- `match` arms have incompatible types
26 |           Some(company) => {
27 | /             people.iter()
28 | |                 .cloned()
29 | |                 .filter(move |p| p.company.clone() == company)
   | |_________________________--------_____________________________- this is found to be of type `Filter<Cloned<std::slice::Iter<'_, Person>>, {closure@src/main.rs:29:25: 29:33}>`
   |                           |
   |                           the expected closure
30 |           },
31 |           None => people.iter().cloned()
   |                   ^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Cloned<Iter<'_, ...>>, ...>`, found `Cloned<Iter<'_, Person>>`
   |
   = note: expected struct `Filter<Cloned<std::slice::Iter<'_, _>>, {closure@src/main.rs:29:25: 29:33}>`
              found struct `Cloned<std::slice::Iter<'_, _>>`
help: you could change the return type to be a boxed trait object
   |
24 | fn specific_company_people(people: &[Person], company: Option<Company>) -> Box<dyn Iterator<Item = Person> + '_> {
   |                                                                            ~~~~~~~                             +
help: if you change the return type to expect trait objects, box the returned expressions
   |
27 ~             Box::new(people.iter()
28 |                 .cloned()
29 ~                 .filter(move |p| p.company.clone() == company))
30 |         },
31 ~         None => Box::new(people.iter().cloned())
   |

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` (bin "playground") due to 1 previous error

match の各枝の評価値は確かに、 filter を挟んでも挟まなくても impl Iterator な型ですが、異なる型です! match ブロックが返す具体的な型は 実行時に決定されます

matchではなく関数の返り値として厳密に調べたもの

「これは関数の返り値型としてではなく match 式の返り値型としてのエラーですよね?関数の返り値型としては大丈夫なのでは?」という勘が鋭い人のために。早期returnバージョンも置いておきます。

Rust
fn specific_company_people(people: &[Person], company: Option<Company>) -> impl Iterator<Item = Person> + '_ {
    if let Some(company) = company {
        return people.iter()
            .cloned()
            .filter(move |p| p.company.clone() == company);
    }

    people.iter().cloned()
}
例全体

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6d02fe71f0f95c6535b6fd43277e856c

Rust
#[allow(unused)]
#[derive(Debug, Clone, PartialEq, Eq)]
enum Company {
    Yumemi,
    Nemumi,
    Samumi,
    Other(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct Person {
    name: String,
    company: Company,
}

// Ok
fn yumemi_people(people: &[Person]) -> impl Iterator<Item = Person> + '_ {
    people.iter()
        .cloned()
        .filter(|p| p.company.clone() == Company::Yumemi)
}

// ダメ!
fn specific_company_people(people: &[Person], company: Option<Company>) -> impl Iterator<Item = Person> + '_ {
    if let Some(company) = company {
        return people.iter()
            .cloned()
            .filter(move |p| p.company.clone() == company);        
    }

    people.iter().cloned()
}

fn main() {
    let people = [
        Person {
            name: "ゆめ太郎".to_string(),
            company: Company::Nemumi,
        },
        Person {
            name: "やめ太郎".to_string(),
            company: Company::Yumemi,
        },
    ];
    
    let mut y_people = yumemi_people(&people);
    
    assert_eq!(&people[1], &y_people.next().unwrap());
    
    let mut n_people = specific_company_people(&people, Some(Company::Nemumi));

    assert_eq!(&people[0], &n_people.next().unwrap());
}
コンパイルエラー
   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:31:5
   |
24 | fn specific_company_people(people: &[Person], company: Option<Company>) -> impl Iterator<Item = Person> + '_ {
   |                                                                            --------------------------------- expected `Filter<Cloned<std::slice::Iter<'_, Person>>, {closure@src/main.rs:28:21: 28:29}>` because of return type
...
28 |             .filter(move |p| p.company.clone() == company);        
   |                     -------- the expected closure
...
31 |     people.iter().cloned()
   |     ^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Cloned<Iter<'_, ...>>, ...>`, found `Cloned<Iter<'_, Person>>`
   |
   = note: expected struct `Filter<Cloned<std::slice::Iter<'_, _>>, {closure@src/main.rs:28:21: 28:29}>`
              found struct `Cloned<std::slice::Iter<'_, _>>`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` (bin "playground") due to 1 previous error

match 式のエラーメッセージの方が親切だったので、こちらは折りたたみました。

よってここでは コンパイル時に型が一つに定まる必要がある RPITの impl Trait は使えず、第18回で登場したような動的ディスパッチ Box<dyn Trait> を使用する必要があります!

Rust (これならおk)
fn specific_company_people(people: &[Person], company: Option<Company>) -> Box<dyn Iterator<Item = Person> + '_> {
    match company {
        Some(company) => {
            Box::new(people.iter()
                .cloned()
                .filter(move |p| p.company.clone() == company))
        },
        None => Box::new(people.iter().cloned())
    }
}

RPIT、あるいは動的ディスパッチであるトレイトを実装した型を返すというのは、イテレータ以外にも特にクロージャを返す際にお世話になるので(筆者の過去の質問リンク)、どちらも知っておいて損はないと思います!というわけで本エクササイズで紹介させていただきました。

クロージャ

クロージャのトレイト(Fn, FnMut, FnOnce)についてもめっちゃ語りたいのですが、エクササイズでは該当回がない(というか今回ぐらいしかない)ですね...RPITや動的ディスパッチを知る機会としてとても恵まれているトピックです。とてもわかりやすい記事があったのでそちらを参照していただけると幸いです。

Rustのクロージャtraitについて調べた(FnOnce, FnMut, Fn) #Rust - Qiita

(紹介した記事でも軽く触れられていますが)一つ筆者なりのコツを紹介しておくと、クロージャは関数として呼び出せる某というよりは「関数と匿名構造体のセット」と考えてみるとわかりやすいです。関数として見た時にシグネチャが同一でも、 記述位置によってキャプチャされる変数は異なる (ゆえに異なる匿名構造体が作られる)から、記述位置によって型が異なるようになっています。逆に 記述位置が同一であれば同一の型です (筆者も謎の勘違いをしていたことがあるのですが、生成毎に異なる型として生成されるわけではありません)。まぁそうじゃないとRPITで返せないですし、 match アームでも型の"比較"なんてできないですよね...

型名がわからない(エイリアスを付ける機能とかできないかな...)のはツラミですが、新たな型がどういう基準で生成されているかを説明できると、先のエクササイズで解説したRPITや動的ディスパッチの解像度も上がるかもしれません。多分。

では次の問題に行きましょう!

次の記事: 【23】 impl Trait ・スライス ~配列の欠片~

  1. 例によってもう少し的確な表現をすると、 FromIterator トレイトを実装している型へと変換できます。 Vec 以外だと HashMap<K, V> でよくお世話になりますね。

  2. 以前紹介した動的ディスパッチの Box<dyn T> を使えば...という話はありますが、それを使うぐらいならとりあえずここは RPIT を利用した方が記述が楽でしょう、という感覚で話を進めています。

  3. 元ネタは2024年に流行った好きな惣菜発表ドラゴンという曲です(動画リンク)

  4. 細かな違いはあるのですがその話題こそ次回扱います。

1
0
0

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
1
0