Yabutan 技術ブログ > Rust mapとand_thenの理解

Rust mapとand_thenの理解

#Rust

2023-04-02

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,

data

// 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>,

data

// 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,

data

// 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>,

data

// 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は処理のチェインやエラー処理を効率的に行いたい場合に使用します。