LoginSignup
2

More than 5 years have passed since last update.

JavaのコンテナクラスっぽいやつをMonadに仕立て上げ、モナド則を満たす

Last updated at Posted at 2015-11-10

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.Responseequals を実装していない。

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 とか必要なメソッドだけ実装するほうが無難なんだろうか…。

追記

MonadLawBindLaw のメソッドが 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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2