map、contramap、imapの整理

3 min read読了の目安(約3100字

この記事について

mapcontramapimap の違いについて整理します。
関数型プログラミングのライブラリとしては cats を使用します。

map

Functormap のシグネチャは以下の通りです。

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

Contravariantcontramap のシグネチャは以下の通りです。

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

contramapShow のような型クラスの入力を変換するのに用いられたりします。


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

Invariantimap のシグネチャは以下の通りです。

def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]

imap を関数に適用する場合、関数の入力と出力の両方 を変換します。 fF[A]の出力を変換する関数で、gF[B] の入力を変換する関数です。
imap の例として、Long の計算規則を利用して、Date の計算規則を作る例を考えてみます。

cats のライブラリには SemigroupLong 用のインスタンスが用意されており、これを利用して 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] の入力を変換する

参考