You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写通用Monad定律测试?解决Scala编译错误问题

解决Scala中Monad定律通用测试的编译错误

问题背景

我们需要为Scala中的Monad实例编写通用的ScalaTest+ScalaCheck测试,验证三条Monad定律(左单位元、右单位元、结合律),避免为每个Monad重复编写测试。但原测试代码中的右单位元和结合律测试无法编译,报错如下:

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定义

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)

原测试代码

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],
    eqFA: Equality[F[B]]
    ): 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))
      }
}

错误原因分析

  1. 混淆Monad实例与Monad值:rightIdentity和associativityLaw中错误地将Monad[F]类型类实例ma当作F[A]类型的具体值使用,实际上ma是用于实现Monad行为的实例,不是待测试的Monad包裹值。
  2. Equality类型不匹配:Equality的类型参数应为F[A](Monad包裹的实际值类型),而非Monad[F](类型类实例)。
  3. unit方法调用冗余:已通过using ma: Monad[F]引入Monad实例,直接调用ma.unit即可,无需额外summon。

修正后的通用测试代码

import org.scalatest.funspec.AnyFunSpec
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.Arbitrary
import org.scalatest.matchers.should.Matchers

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

  // 右单位元定律: m.flatMap(unit) === m
  def rightIdentity[A: Arbitrary](
    using arbFA: Arbitrary[F[A]],
    ma: Monad[F],
    eqFA: Equality[F[A]]
  ): Unit =
    it("should satisfy right identity law") {
      forAll { (m: F[A]) =>
        m.flatMap(ma.unit) shouldBe m
      }
    }

  // 结合律: m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))
  def associativityLaw[A: Arbitrary, B: Arbitrary, C: Arbitrary](
    using arbFA: Arbitrary[F[A]],
    arbAToFB: Arbitrary[A => F[B]],
    arbBToFC: Arbitrary[B => F[C]],
    ma: Monad[F],
    eqFC: Equality[F[C]]
  ): Unit =
    it("should satisfy associativity law") {
      forAll { (m: F[A], f: A => F[B], g: B => F[C]) =>
        val left = m.flatMap(f).flatMap(g)
        val right = m.flatMap(x => f(x).flatMap(g))
        left shouldBe right
      }
    }
}

特定Monad实例测试示例(Option)

import org.scalacheck.Arbitrary
import org.scalatest.matchers.should.Matchers

class OptionMonadLawsSpec extends AnyFunSpec with ScalaCheckPropertyChecks with Matchers with MonadLaws[Option] {
  describe("Option Monad") {
    import MonadInstances.optionMonad
    import org.scalacheck.Arbitrary.arbitrary

    // 为Option提供Arbitrary生成规则
    given [A: Arbitrary]: Arbitrary[Option[A]] = Arbitrary(
      arbitrary[A].map(Option(_)) | arbitrary[Unit].map(_ => None)
    )
    // 为Option提供Equality判断逻辑
    given [A]: Equality[Option[A]] = (a: Option[A], b: Any) => b match {
      case opt: Option[A] => a == opt
      case _ => false
    }

    leftIdentity[Int, String]
    rightIdentity[Int]
    associativityLaw[Int, String, Boolean]
  }
}

// Option的Monad实例实现
object MonadInstances {
  given optionMonad: Monad[Option] with {
    override def unit[A](a: => A): Option[A] = Some(a)

    extension [A](fa: Option[A])
      override def flatMap[B](f: A => Option[B]): Option[B] = fa.flatMap(f)
  }
}

内容的提问来源于stack exchange,提问作者Abhijit Sarkar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 20:42:03