
mapメソッドの話
この記事は公開されてから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 メソッドについて、そのさわりを書いてみました。
「もっと具体的に知りたい」と思った方は、以下の参考リンクから旅をしてみてください。それはきっと長く、そして楽しい旅になると思います。
ではまた!











