この記事は Rust Advent Calendar 2019 の9日目の記事です。
最近、すごいHaskellを読み終わりました。
HaskellやってからRustに戻ってくるとmapとand_thenの違いがわかってよいな
— さいぺ (@cipepser) 2019年10月15日
Rustに戻ってきたので、改めてmap
とand_then
の違いを整理したいと思います。モナドで一般化されますが、本記事ではOption
型を例に考えます。
環境
❯ rustup --version rustup 1.20.2 (13979c968 2019-10-16) ❯ cargo version cargo 1.38.0 (23ef9a4ef 2019-08-20)
実装の確認
まずはmap
とand_then
がどのように実装されているか見てみましょう。以下は、libcore/option.rs
の実装です。
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> { match self { Some(x) => Some(f(x)), None => None, } } pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U> { match self { Some(x) => f(x), None => None, } }
クロージャF
のシグネチャに注目します。
FnOnce
を無視すると以下のようになっています。
map
:T -> U
and_then
:T -> Option<U>
例として、値を2倍にするようなクロージャを考えると、以下のような感じでしょうか。
map
は値をそのまま返し、and_then
はSome
で包んで返します。
fn main() { let m = Some(1).map(|x| x * 2); println!("{:?}", m); // Some(2) let a = Some(1).and_then(|x| Some(x * 2)); println!("{:?}", a); // Some(2) }
上記の例のように単純に値を写像させたいだけの場合は、map
が便利なのですが、複数の(Option
を返す)関数をチェインさせたい場合は、and_then
が真価を発揮します。1
例
複数の関数をチェインさせる例として、以下のようなDBに対して、id
からname
を引き、name
からprice
を得ることを考えます。
PRODUCTSテーブル
id | name |
---|---|
0 | desk |
1 | chair |
2 | whiteboard |
3 | pencil |
PRICEテーブル
name | price |
---|---|
desk | 300 |
chair | 100 |
whiteboard | 20 |
実装
DBの代替にHashMap
を使います。
lazy_static! { static ref PRODUCTS: HashMap<u32, &'static str> = { let mut m = HashMap::new(); m.insert(0, "desk"); m.insert(1, "chair"); m.insert(2, "whiteboard"); m.insert(3, "pencil"); m }; static ref PRICE: HashMap<&'static str, u32> = { let mut m = HashMap::new(); m.insert("desk", 300); m.insert("chair", 100); m.insert("whiteboard", 20); m }; }
id
からname
を得る関数をget_product
、name
からprice
を得る関数をget_price
とします。
DB上にレコードが存在しない可能性があるので返り値がOption
型であることに注意してください。
fn get_product(id: u32) -> Option<&'static str> { match PRODUCTS.get(&id) { Some(u) => Some(u), None => None, } } fn get_price(product: &'static str) -> Option<u32> { match PRICE.get(&product) { Some(u) => Some(*u), None => None, } }
get_product
とget_price
をチェインさせる
まずはmap
を使った場合です。
fn main() { let product_id = 1; let price = get_product(product_id) .map(get_price); println!("{:?}", price); // Some(Some(100)) }
Some
が二重に包まれてしまいました。値を取り出したいときに何度もunwrap()
しないといけないですね。
一方でand_then
はどうでしょう。
fn main() { let product_id = 1; let price = get_product(product_id) .and_then(get_price); println!("{:?}", price); // Some(100) }
Some
に包まれるのは一回だけです。
失敗するパターン
もう一度DBのテーブルたちを再掲します。
PRODUCTSテーブル
id | name |
---|---|
0 | desk |
1 | chair |
2 | whiteboard |
3 | pencil |
PRICEテーブル
name | price |
---|---|
desk | 300 |
chair | 100 |
whiteboard | 20 |
PRODUCTS
テーブルには、pencil
が存在しますが、PRICE
テーブルには存在しません。
id=3
でmap
とand_then
の挙動を比較してみましょう。
fn main() { let product_id = 3; let price = get_product(product_id) .map(get_price); println!("{:?}", price); // Some(None) }
fn main() { let product_id = 3; let price = get_product(product_id) .and_then(get_price); println!("{:?}", price); // None }
map
では、Some(None)
が、and_then
ではNone
となりました。
まとめ
関数をチェーンさせる場合、map
を使うと多重にOption
で包まれてしまうことを確認しました。
map
を連続で使っていると最終的に何回包まれたのか覚えておかなくてはなりません。
さらに、何かしらの理由でget_product
とget_price
の間に、別のOption
を返す関数を実装しなくてはならなくなった場合はどうでしょう。
コンビネータを差し込むだけではなく、包みを解くコードにも変更が必要です。
差し込む関数を実装するときも、前の関数が何回Option
を包んだのか意識しながら実装することになり、変更に弱くなります。
References
-
Rust by Exampleにも書いてあります。↩