【Rust】mapとand_thenの違い
この記事は 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 -> Uand_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にも書いてあります。↩