前の記事
- 【0】 準備 ← 初回
- ...
- 【21】 イテレータ・ライフタイム ~ライフタイム注釈ようやく登場!~ ← 前回
- 【22】 コンビネータ・RPIT ~ 「
Iterator
トレイトを実装してるやつ」~ ← 今回
全記事一覧
- 【0】 準備
- 【1】 構文・整数・変数
- 【2】 if・パニック・演習
- 【3】 可変・ループ・オーバーフロー
- 【4】 キャスト・構造体 (たまにUFCS)
- 【5】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~
- 【6】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~
- 【7】 スタック・ヒープと参照のサイズ ~メモリの話~
- 【8】 デストラクタ(変数の終わり)・トレイト ~終わりと始まり~
- 【9】 Orphan rule (孤児ルール)・演算子オーバーロード・derive ~Empowerment 💪 ~
- 【10】 トレイト境界・文字列・Derefトレイト ~トレイトのアレコレ~
- 【11】 Sized トレイト・From トレイト・関連型 ~おもしろトレイトと関連型~
- 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~
- 【13】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~
- 【15】 Result型 ~Rust流エラーハンドリング術~
- 【16】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~
- 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~
- 【18】 Errorのネスト・慣例的な書き方 ~Rustらしさの目醒め~
- 【19】 配列・動的配列 ~スタックが使われる配列と、ヒープに保存できる動的配列~
- 【20】 動的配列のリサイズ・イテレータ ~またまたトレイト登場!~
- 【21】 イテレータ・ライフタイム ~ライフタイム注釈ようやく登場!~
- 【22】 コンビネータ・RPIT ~ 「
Iterator
トレイトを実装してるやつ」~ - 【23】
impl Trait
・スライス ~配列の欠片~ - 【24】 可変スライス・下書き構造体 ~構造体で状態表現~
- 【25】 インデックス・可変インデックス ~インデックスもトレイト!~
- 【26】 HashMap・順序・BTreeMap ~Rustの辞書型~
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~
- 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~
- 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと
Rc<RefCell<T>>
~ - 【30】 双方向通信・リファクタリング ~返信用封筒を入れよう!~
- 【31】 上限付きチャネル・PATCH機能 ~パンクしないように制御!~
- 【32】
Send
・排他的ロック(Mutex
)・非対称排他的ロック(RwLock
) ~真打Arc<Mutex<T>>
登場~ - 【33】 チャネルなしで実装・Syncの話 ~考察回です~
- 【34】
async fn
・非同期タスク生成 ~Rustの非同期入門~ - 【35】 非同期ランタイム・Futureトレイト ~非同期のお作法~
- 【36】 ブロッキング・非同期用の実装・キャンセル ~ラストスパート!~
- 【37】 Axumでクラサバ! ~最終回~
- 【おまけ1】 Rustで勘違いしていたこと3選 🏄🌴 【100 Exercises To Learn Rust 🦀 完走記事 🏃】
- 【おまけ2】 【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か
100 Exercise To Learn Rust 演習第22回になります!
今回の関連ページ
[06_ticket_management/07_combinators] Combinators イテレータのコンボ技!
問題はこちらです。
// 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なんかにもある map
や filter
等の高階関数の回です! for
文を使わないでなるべく高階関数でなんでも済ませようって風潮、どの言語にもありますよね(?)
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!」みたいなことを言ったと思うのですが、「よし、じゃあ連結させやすいように"イテレータ"を返すようにしよう!」という問題です。
テストを含めた全体
// 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 メソッドの返り値型になりそうということで、意気揚々と型を書いてみますが...
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
を実装した型のことを言うのでした。
最終的にはこんな風に書きたいということになります。
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) です!さっきの好きな型発表ドラゴン的な書き方をそのまま実現した文法です!(演習の答えになります)
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 Trait
の Trait
を満たす型を返しうる」ようにはできません!
ちょっと言葉だとわかりにくいので...例えばこれはコンパイルエラーです!(エクササイズに合わせてイテレータの問題にしたらちょっと複雑になってしまった...)
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
#[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バージョンも置いておきます。
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
#[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>
を使用する必要があります!
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
・スライス ~配列の欠片~
-
例によってもう少し的確な表現をすると、
FromIterator
トレイトを実装している型へと変換できます。Vec
以外だとHashMap<K, V>
でよくお世話になりますね。 ↩ -
以前紹介した動的ディスパッチの
Box<dyn T>
を使えば...という話はありますが、それを使うぐらいならとりあえずここは RPIT を利用した方が記述が楽でしょう、という感覚で話を進めています。 ↩ -
細かな違いはあるのですがその話題こそ次回扱います。 ↩