この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
mapメソッドの話
今回は、普段よくつかう map
メソッドについてお話します。
本記事は「ポエム特集」のカテゴリの記事であり、原典とする書籍等が存在するものではありません。予めご承知おきください。
mapのoverview
List#map
は普段よく使います。
任意の関数を引数にとり、各要素にそれぞれ適用した結果のリストを返します。
scala> List(1, 2, 3).map(_ + 10)
res1: List[Int] = List(11, 12, 13)
Option#map
は、その次によく使います(よね?)。
こちらは自身が Some(x)
で引数の関数が f
のとき、 Some(f(x))
を返します。
また、自身が None
であれば直ちに None
を返します。以前これを「文脈を保つ特徴」と説明しました。
scala> Some(1).map(_ + 10)
res2: Option[Int] = Some(11)
scala> Option.empty[Int].map(_ + 10)
res3: Option[Int] = None
このような特徴は map
メソッドを持つ他の型についても言えます。
scala> List.empty[Int].map(_ + 10)
res4: List[Int] = List()
#import文は省略
scala> val fa = Future.failed[Int](new RuntimeException).map(_ + 10)
fa: scala.concurrent.Future[Int] = Failure(java.lang.RuntimeException)
scala> Await.result(fa, Duration.Inf)
java.lang.RuntimeException
... 32 elided
mapを作る
いままで紹介した map
は全て、もともと存在するメソッドでした。
final case class Wrap[+A](get: A)
// example
val wa = Wrap(123)
wa.get // => 123
val wb = Wrap("xxx")
wb.get // => "xxx"
いま、私たちは簡素な型 Wrap
を作りました。この新しい型に map
メソッドを与えてみましょう。
final case class Wrap[+A](get: A) {
def map[B](f: A => B): Wrap[B] = Wrap(f(get))
}
// example
Wrap(123).map(x => s"The number is $x").get // => "The number is 123"
多くの方が、直感的に実装を書くことができると思います。
じゃあ次はこんな型を作って見ます。
sealed trait ColorWrap[+A]
final case class BlueWrap[+A](get: A) extends ColorWrap[A]
final case class RedWrap[+A](get: A) extends ColorWrap[A]
final case class GreenWrap[+A](get: A) extends ColorWrap[A]
色のついた wrap で ColorWrap
です。 colored wrap にしたほうが英語っぽいですが細かいことは気にしません。
これも多くの方が直感的に map
を書けると思います。
sealed trait ColorWrap[+A] {
def map[B](f: A => B): ColorWrap[B] = this match {
case BlueWrap(v) => BlueWrap(f(v))
case RedWrap(v) => RedWrap(f(v))
case GreenWrap(v) => GreenWrap(f(v))
}
}
final case class BlueWrap[+A](get: A) extends ColorWrap[A]
final case class RedWrap[+A](get: A) extends ColorWrap[A]
final case class GreenWrap[+A](get: A) extends ColorWrap[A]
こんな風に。 f
を適用しつつ、青ならば青を、赤ならば赤を返すようなメソッドを書くと思います。
この「青なら map
しても青のまま」という特徴は、先ほど私たちが「文脈を保つ」と表現したものでしょう。私たちは(大抵のばあい)それを経験的に感じ取っています。
ところで。もし直感に抗っていいなら、青だろうが赤だろうが必ず緑を返す map
も作れますよね。
sealed trait ColorWrap[+A] {
def map[B](f: A => B): ColorWrap[B] = this match {
case BlueWrap(v) => GreenWrap(f(v))
case RedWrap(v) => GreenWrap(f(v))
case GreenWrap(v) => GreenWrap(f(v))
}
}
文脈は保たれなくなりましたが、コンパイル通るんだから何も問題ないですね?
私たちの map
が緑ばかり返すことを、いったい誰が咎めると言うのだろうか。
mapの決まりごと
結論から言ってしまいます。 map
メソッドを作る際には「守らなければならない決まり」があります。
hoge
と 「hoge.map(…)
に恒等関数(引数をそのまま返す関数)をつっこんだもの」 が等しいhoge.map(f).map(g)
とhoge.map(x => g(f(x)))
が等しい
この決まりを守ってない map
は嘘つきです。さぞ皆から咎められるでしょう。
今まで、「文脈を保つ」という直感的な表現をしていた map
の特徴は、主に 1. の決まりによって遵守されます。
さきほどの、緑ばかり返す ColorWrap#map
を思い出してください。
// 緑ばかり返す map を使う例
// Note: x => x は identity とも書けます
// 1.
val blue = BlueWrap(123)
blue == blue.map(x => x) // => false; blue.map(x => x) は GreenMap(123) を返します
// 2.
val f: Int => Int = _ + 1
val g: Int => String = "v=" + _
blue.map(f).map(g) // => GreenWrap(v=124)
blue.map(x => g(f(x))) // => GreenWrap(v=124)
blue
と blue.map
に恒等関数をつっこんだものが等しくないです。これでは嘘つきの map
です。
(2. は守られていますが、 1. は守られていません)
一方、ColorWrap
に最初に与えた、直感的な map
メソッドはこの規則を守っています。
// それぞれの色を返す map を使う例
// Note: x => x は identity とも書けます
val blue = BlueWrap(123)
blue == blue.map(x => x) // => true
val red = RedWrap("aaa")
red == red.map(x => x) // => true
もちろん 2. も守られています *1。ぜひ試してみてください。
まとめ
map
メソッドが(直感的に)「文脈を保つ」ことは、map
メソッドの決まりごとによって保証されています。map
メソッドを自分で作るときは、この法則を守るようにしてください。みんな幸せになります。
今回は、ふだん何気なく使っている map
メソッドについて、そのさわりを書いてみました。
「もっと具体的に知りたい」と思った方は、以下の参考リンクから旅をしてみてください。それはきっと長く、そして楽しい旅になると思います。
ではまた!