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