Rustプログラミング言語では、Option、Resultといった型が頻繁に使用され、 それらに対して関数を適用する方法としてmapとand_thenが提供されています。 この記事では、これらのメソッドの違いをわかりやすくイメージ化して解説します。
mapとand_thenの目的の違い
mapは、主に値の変換を目的として使われます。 OptionやResultの中の値に関数を適用して新しい値を作成するイメージです。 一方、and_thenは処理のチェインにフォーカスしている。 関数がOptionやResult型を返す場合に、ある(Some or Ok)なら処理するというもので、 チェインしていく中で、値が利用できなくなったりエラーが発生した場合に処理を効率的に中断できます。 目的が微妙に異なる二つですが、関数の実態はすごく似ているので、どちらを使うのかよくわからなくなることもあります。
シグネチャの違い
mapと、and_thenの違いは、引数に渡すラムダ式の返り値の型が少し違うだけです。 mapは、値そのものを返すのに対して、and_thenは、Option、もしくはResultを返します。 各シグネチャの違いと、イメージ図、実際のコードを以下に示します。
Option::map
fn map<V, F>(self, f: F) -> Option<V>
where
F: FnOnce(T) -> V,
// Option<i32> -> map -> Option<String>
let v: Option<i32> = Some(1);
let v: Option<String> = v.map(|x| x.to_string());
assert_eq!(v, Some("1".to_string()));
// ==> ラムダ式を通って返した値がSomeでラップされている。
let v: Option<i32> = None;
let v: Option<String> = v.map(|x| x.to_string());
assert_eq!(v, None);
// ==> もともと、Noneなのでラムダ式は通ってない。
値がある(Some)場合に、何かしらしてTからVに変換しているだけですね。 TとVは同じ型であっても問題ありません。 なければ(None)何もせずにNoneを返してきます。
Option::and_then
fn and_then<V, F>(self, f: F) -> Option<V>
where
F: FnOnce(T) -> Option<V>,
// Option<i32> -> and_then -> Option<String>
let f = |x: i32| -> Option<String> {
if x > 0 {
Some(x.to_string())
} else {
None
}
};
let v: Option<i32> = Some(1);
let v: Option<String> = v.and_then(f);
assert_eq!(v, Some("1".to_string()));
// ==> ラムダ式を通ってSomeを返してる。
let v: Option<i32> = Some(0);
let v: Option<String> = v.and_then(f);
assert_eq!(v, None);
// ==> ラムダ式を通ってNoneを返してる。
let v: Option<i32> = None;
let v: Option<String> = v.and_then(f);
assert_eq!(v, None);
// ==> もともと、Noneなのでラムダ式は通ってない。
値がある(Some)場合に、何かしらしてTからVにしているのは同じですが、 ラムダ式でSome と、Noneのどちらかを返すことができるようになっています。 もともと、Noneであれば何もせずにそのままNoneを返してきます。
Result::map
fn map<V, F>(self, op: F) -> Result<V, E>
where
F: FnOnce(T) -> V,
// Result<i32, String> -> map -> Result<String, String>
let v: Result<i32, String> = Ok(1);
let v: Result<String, String> = v.map(|x| x.to_string());
assert_eq!(v, Ok("1".to_string()));
// ==> ラムダ式を通って返した値がOkでラップされている。
let v: Result<i32, String> = Err("error".to_string());
let v: Result<String, String> = v.map(|x| x.to_string());
assert_eq!(v, Err("error".to_string()));
// ==> もともと、Errなのでラムダ式は通ってない。
Result版のmapです。 成功(Ok)の場合に、何かしらしてTからVに変換しています。 TとVは同じ型であっても問題ありません。 もともとがErrであれば、何もせずにErrを返してきます。
Result::and_then
fn and_then<V, F>(self, op: F) -> Result<V, E>
where
F: FnOnce(T) -> Result<V, E>,
// Result<i32, String> -> and_then -> Result<String, String>
let f = |x: i32| -> Result<String, String> {
if x > 0 {
Ok(x.to_string())
} else {
Err("error".to_string())
}
};
let v: Result<i32, String> = Ok(1);
let v: Result<String, String> = v.and_then(f);
assert_eq!(v, Ok("1".to_string()));
// ==> ラムダ式を通ってOkを返してる。
let v: Result<i32, String> = Ok(0);
let v: Result<String, String> = v.and_then(f);
assert_eq!(v, Err("error".to_string()));
// ==> ラムダ式を通ってErrを返してる。
let v: Result<i32, String> = Err("error".to_string());
let v: Result<String, String> = v.and_then(f);
assert_eq!(v, Err("error".to_string()));
// ==> もともと、Errなのでラムダ式は通ってない。
成功(Ok)場合に、何かしらしてTからVにしているのは同じですが、 ラムダ式でOk と、Errのどちらかを返すことができるようになっています。 エラーの型は変わらないことに注意です。 もともと、Errであれば何もせずにそのままErrを返してきます。
まとめ
mapとand_thenはコンセプトは違えど、シグネチャはほとんど同じになっています。 たまに頭がこんがらがってきますが、mapは値の変換を行いたい場合に使用、and_thenは処理のチェインやエラー処理を効率的に行いたい場合に使用します。