map、contramap、imapの整理
この記事について
map と contramap と imap の違いについて整理します。
関数型プログラミングのライブラリとしては cats を使用します。
map
Functor の map のシグネチャは以下の通りです。
def map[A, B](fa: F[A])(f: A => B): F[B]
これを関数に適用すると、関数の出力 を(型を含め)変換することができます。
import cats._
import cats.implicits._
type IntToX[X] = Int => X
val functor = implicitly[Functor[IntToX]]
val f: Int => String = _.toString
val multiply2: Int => Int = functor.map(f)(x => x.toInt * 2)
multiply2(3)
// 6
map はお馴染みなのでいうまでもありませんが、List の要素や Future の出力の変換にも用いることができます。
contramap
Contravariant の contramap のシグネチャは以下の通りです。
def contramap[A, B](fa: F[A])(f: B => A): F[B]
これを関数に適用すると、関数の入力 を変換することができます。
import cats._
import cats.implicits._
import scala.io._
type XtoInt[X] = X => Int
val contravariant = implicitly[Contravariant[XtoInt]]
val f: XtoInt[String] = _.toInt
val readLine: Unit => String = () => StdIn.readLine()
val readInt: () => Int = contravariant.contramap(f)(readLine)
readInt() // input "300"
// 300
contramap は Show のような型クラスの入力を変換するのに用いられたりします。
case class Money(amount: Int)
case class Salary(size: Money)
// これはMoneyを受け取ってStringを返すShowのインスタンス
implicit val showMoney: Show[Money] = Show.show(m => s"$$${m.amount}")
// Salaryを受け取ってStringを返すShowのインスタンスを作るのにMoneyのインスタンスを利用する
// このとき、contramap(Salary => Money)を利用する (Money => StringをSalary=>Moneyという入力変換の関数を使ってSalary => Stringに変換している)
implicit def showSalary: Show[Salary] = implicitly[Show[Money]].contramap(_.size)
imap
Invariant の imap のシグネチャは以下の通りです。
def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]
imap を関数に適用する場合、関数の入力と出力の両方 を変換します。 f は F[A]の出力を変換する関数で、g は F[B] の入力を変換する関数です。
imap の例として、Long の計算規則を利用して、Date の計算規則を作る例を考えてみます。
cats のライブラリには Semigroup の Long 用のインスタンスが用意されており、これを利用して Long の加算を行うことができます。
import cats._
import cats.implicits
100000L |+| 50000L // Semigroup[Long]を利用した加算
しかし、Date 用の型クラスのインスタンスは用意されていないので、このままでは加算を行えません。
import java.util.Date
val d1 = new Date(100000L)
val d2 = new Date(130000L)
d1 |+| d2
// error: value |+| is not a member of java.util.Date
コンストラクタに渡している Long 同士の計算ができることはわかっているので、Semigroup[Long] を利用して Semigroup[Date] を作る方法を考えます。
まず、Long 同士の計算結果は以下の関数を渡せば Date 型に変換できます。(map の計算)
val f: Long => Date = l => new Date(l)
目的となる Semigroup[Date] が入力となる Date を受け取ったときに、それを Long に変換する関数は以下のようになります。(contramap の計算)
val g: Date => Long = _.getTime
imap を利用すると、上記の計算ステップを利用して Date 用の Semigroup のインスタンスを得ることができます。
implicit val sl: Semigroup[Long] = implicitly[Semigroup[Long]]
implicit val sd: Semigroup[Date] = implicitly[Invariant[Semigroup]].imap(sl)(f)(g)
d1 |+| d2
// ok
まとめ
幾分か正確さに欠けるけど、以下のような整理で捉えておいてよいのではないかと思います。
-
mapは出力を変換する -
contramapは入力を変換する -
imapは元のF[A]の出力を変換し、ターゲットとなるF[B]の入力を変換する
Discussion