如何编写通用 Monad 定律测试?

问题描述 投票:0回答:1

给出定义:

trait Functor[F[_]]:
  extension [A](fa: F[A]) def map[B](f: A => B): F[B]

trait Monad[F[_]] extends Functor[F]:
  def unit[A](a: => A): F[A]

  extension [A](fa: F[A])
    def flatMap[B](f: A => F[B]): F[B]

    def map[B](f: A => B): F[B] =
      flatMap(f.andThen(unit))

object MonadSyntax:
  // 'fA' is the thing on which the method >>= is invoked.
  extension [F[_], A](fA: F[A])(using mA: Monad[F])
    def >>=[B](f: A => F[B]): F[B] = mA.flatMap(fA)(f)

我想使用 ScalaTest 和 ScalaCheck 编写测试来验证 Monad 三个定律。我不想对每个 Monad 实例重复测试,而是想让测试变得通用。我的尝试如下:

trait MonadLaws[F[_]] { this: AnyFunSpec & ScalaCheckPropertyChecks =>
  // Monad[F].unit(x).flatMap(f) === f(x)
  def leftIdentity[A: Arbitrary, B: Arbitrary](
    using arbAToFB: Arbitrary[A => F[B]], 
    ma: Monad[F]
    ): Unit =
    it("should satisfy the left identity law"):
      forAll { (x: A, f: A => F[B]) =>
        (ma.unit(x) >>= f) shouldBe f(x)
      }

  // m.flatMap(Monad[F].unit) === m
  def rightIdentity[A: Arbitrary](using eqM: Equality[Monad[F]], ma: Monad[F]): Unit =
    it("should satisfy the right identity law"):
        val left = for
          a: A <- ma
        yield summon[Monad[F]].unit(a)

      left shouldBe ma

  // m.flatMap(f).flatMap(g) === m.flatMap { x => f(x).flatMap(g) }
  def associativityLaw[A: Arbitrary, B: Arbitrary, C: Arbitrary](
    using arbAToFB: Arbitrary[A => F[B]],
    arbBToFB: Arbitrary[B => F[C]],
    ma: Monad[F],
    eqM: Equality[Monad[F]]
  ): Unit =
    it("should satisfy the associativity law"):
      forAll { (f: A => F[B], g: B => F[C]) =>
        ma.flatMap(f).flatMap(g) shouldBe m.flatMap(x => f(x).flatMap(g))
      }
}

然后一个特定的 Monad 实例将作为测试运行,如下所示:

class OptionMonadLawsSpec extends AnyFunSpec with ScalaCheckPropertyChecks with MonadLaws[Option]:
  describe("Options form a Monad"):
    import MonadInstances.optionMonad

    leftIdentity[Int, Int]

为简洁起见,省略了导入。

问题是第二个和第三个测试无法编译,都失败了:

Found:    A => F[A]
Required: F[A²]

where:    A  is a type in method rightIdentity
          A² is a type variable with constraint 
          F  is a type in trait MonadLaws with bounds <: [_] =>> Any

我绞尽脑汁一个多小时了,还是没能把类型搞对。另外,如何调用

Monad[F].unit
,其中
unit
是实例方法?

scala functional-programming monads scala-3
1个回答
0
投票

@LuisMiguelMejíaSuárez 在 Typelevel Discord 频道上帮助我编译代码。我还没有彻底测试它,但类型检查出来了,他也很友善地指出了一个基本的误解。

trait MonadLaws[F[_]] { this: AnyFunSpec & ScalaCheckPropertyChecks =>
  // Monad[F].unit(x).flatMap(f) === f(x)
  def leftIdentity[A, B](using
      Monad[F],
      Arbitrary[A],
      Arbitrary[A => F[B]],
      Equality[F[B]]
  ): Unit =
    it("should satisfy the left identity law"):
      forAll { (a: A, f: A => F[B]) =>
        val lhs = summon[Monad[F]].unit(a) >>= f
        val rhs = f(a)

        lhs shouldBe rhs
      }

  // m.flatMap(Monad[F].unit) === m
  def rightIdentity[A, B](using Monad[F], Arbitrary[F[A]], Equality[F[A]]): Unit =
    it("should satisfy the right identity law"):
      forAll { (fa: F[A]) =>
        val lhs = fa >>= summon[Monad[F]].unit
        val rhs = fa

        lhs shouldBe rhs
      }

  // m.flatMap(f).flatMap(g) === m.flatMap { x => f(x).flatMap(g) }
  def associativityLaw[A, B, C](using
      Monad[F],
      Arbitrary[F[A]],
      Arbitrary[A => F[B]],
      Arbitrary[B => F[C]],
      Equality[F[C]]
  ): Unit =
    it("should satisfy the associativity law"):
      forAll { (fa: F[A], f: A => F[B], g: B => F[C]) =>
        val lhs = fa >>= f >>= g
        val rhs = fa >>= (a => f(a) >>= (g))

        lhs shouldBe rhs
      }
}
© www.soinside.com 2019 - 2024. All rights reserved.