mapメソッドの話

Scala

この記事は公開されてから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 メソッドを作る際には「守らなければならない決まり」があります。

  1. hoge と 「 hoge.map(…) に恒等関数(引数をそのまま返す関数)をつっこんだもの」 が等しい
  2. 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)

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

ではまた!

参考

脚注

  1. 私たちが普段プログラミングで触れられる範囲に限定すれば、1. ⇒ 2. と言えるようです。(参考

AWS Cloud Roadshow 2017 福岡