「redis.clients.jedis.Response を Functor, Monad に仕立て上げる」の補足記事。
モナドに仕立て上げたつもりがモナド則を満たせてなかった?
モナド則を満たせているか検証
scala> import scalaz.Scalaz._
scala> import redis.clients.jedis.Response
scala> import com.zaneli.redis.jedis.responseMonad
scala> val m = 10.point[Response]
scala> def f(x: Int): Response[Int] = (x * 10).point[Response]
scala> def g(x: Int): Response[Int] = (x + 1).point[Response]
scala> 10.point[Response].flatMap(f) == f(10)
res1: Boolean = false
scala> m.flatMap(_.point[Response]) == m
res2: Boolean = false
scala> m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))
res3: Boolean = false
あれれ…?
と思ったけど、それもそのはず、そもそもJavaのredis.clients.jedis.Responseが equals
を実装していない。
scala> 10.point[Response] == 10.point[Response]
res4: Boolean = false
こういう場合どうするのがいいんだろう…。とりあえず scalaz.Equal
を使ってみる。
scalaz.Equal の導入
package com.zaneli.redis
import redis.clients.jedis.{Builder, Response}
import redis.clients.jedis.exceptions.JedisDataException
import scalaz.{Equal, Monad}
package object jedis {
implicit val responseMonad = new Monad[Response] {
override def point[A](a: => A): Response[A] = {
val r = new Response(new Builder[A] {
override def build(o: scala.Any): A = o.asInstanceOf[A]
})
r.set(a)
r
}
override def bind[A, B](fa: Response[A])(f: A => Response[B]): Response[B] = {
val r = new Response[B](new Builder[B] {
override def build(o: scala.Any): B = f(o.asInstanceOf[Response[A]].get).get
})
r.set(fa)
r
}
}
implicit def responseEqual[A]: Equal[Response[A]] = new Equal[Response[A]] {
override def equal(a1: Response[A], a2: Response[A]): Boolean = {
try {
a1.get == a2.get
} catch {
case _: JedisDataException => false
}
}
}
}
これにより ===
で中身の値を比較することができる。
scala> import com.zaneli.redis.jedis.responseEqual
scala> 10.point[Response].flatMap(f) === f(10)
res5: Boolean = true
scala> m.flatMap(_.point[Response]) === m
res6: Boolean = true
scala> m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))
res7: Boolean = true
疑問は残る…
うーむ、しかし果たしてこれでいいのか。
まず、 ==
が false
な場合に ===
が true
となる挙動には問題はないのだろうか。
また、モナド則を満たしていないものをモナドインスタンスにすると、モナド則を満たしていることを前提にした処理が思わぬところで想定通り挙動しないという認識だが、==
での一致を前提としていれば結局今回の対応ではモナド則を満たせたことにならないのではないか。
Javaのコンテナクラスっぽいやつ?(値を保持する入れ物クラスっぽいやつ)をモナドにするというのが、そもそも悪手なんだろうか。
中身の値を使いたいだけなら、モナドインスタンスにせずに独自に flatMap
とか必要なメソッドだけ実装するほうが無難なんだろうか…。
追記
MonadLaw
や BindLaw
のメソッドが implicit FA: Equal[F[A]]
を取るので、今回実装した方針で概ね間違っていなさそう?
scala> import scalaz.Scalaz._
scala> import redis.clients.jedis.Response
scala> import com.zaneli.redis.jedis._
scala> val m = 10.point[Response]
scala> def f(x: Int): Response[Int] = (x * 10).point[Response]
scala> def g(x: Int): Response[Int] = (x + 1).point[Response]
scala> responseMonad.monadLaw.rightIdentity(m)
res1: Boolean = true
scala> responseMonad.monadLaw.leftIdentity(10, f)
res2: Boolean = true
scala> responseMonad.monadLaw.associativeBind(m, f, g)
res3: Boolean = true