如何编写通用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)) } }
错误原因分析
- 混淆Monad实例与Monad值:
rightIdentity和associativityLaw中错误地将Monad[F]类型类实例ma当作F[A]类型的具体值使用,实际上ma是用于实现Monad行为的实例,不是待测试的Monad包裹值。 - Equality类型不匹配:
Equality的类型参数应为F[A](Monad包裹的实际值类型),而非Monad[F](类型类实例)。 - 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
相关产品推荐
相关产品推荐

