Scala:为什么我的 Scala 测试自定义匹配器无法编译?

Scala: Why does my Scala Test custom matcher not compile?

我正在做书中的练习 Learning Scala,有一个问题问:

Create a container class that has an instance of itself plus an instance of a parameterized type. The constructor should take a variable number of the instances (e.g., strings or ints or any other parameterized type), which can be implemented with vararg parameters.

我得出以下结论:

sealed abstract class MyList[A]

sealed class MyNil[A] extends MyList[A]

sealed case class Cons[A](h: A, t: MyList[A]) extends MyList[A]

object MyNil {
  def apply[A] = new MyNil[A]
}

object MyList {
  def apply[A](items: A*): MyList[A] = {
    items match {
      case h :: t => Cons[A](h, apply(t: _ *))
      case _ => MyNil[A]
    }
  }
}

到目前为止,还不错。问题在于测试此代码。以下使用 Scala 测试和自定义匹配器的测试无法编译,更具体地说是 isEqual 方法。你能帮我理解为什么吗?

class MyListSpec extends FlatSpec with Matchers {
  "MyList" should "be instantiated as expected" in {
    val inputAndOutput = Table(
      ("ip", "op"),
      (Nil, MyNil[Int]),
      (List(1), Cons[Int](1, MyNil[Int])),
      (List(1, 2), Cons[Int](1, Cons[Int](2, MyNil[Int]))),
      (List(1, 2, 3), Cons[Int](1, Cons[Int](2, Cons[Int](3, MyNil[Int]))))
    )

    forAll(inputAndOutput) { (ip, op) =>
      val o = MyList(ip: _ *)
      println(o)

      o should equal(op)
    }
  }

  def equal[Int](right: MyList[Int]) = new MyListMatcher(right)
}

class MyListMatcher[Int, A <: MyList[Int]](val right: A) extends Matcher[A] {
  override def apply(left: A): MatchResult = {
    MatchResult(
      isEqual(left),
      s"""MyList $left did not match "$right"""",
      s"""MyList $left matched "$right""""
    )
  }

  def isEqual[A <: MyList[Int]](left: A) = {
    left match {
      case _: MyNil[Int] if right.isInstanceOf[MyNil[Int]] => println("Both are Nil."); true
      case Cons[Int](h, t) if right.isInstanceOf[Cons[Int]] => {
        println("Both are Cons.")
        left == right
      }
      case _ => println("No match."); false
    }
  }
}

编辑 1: 根据要求包含来自 IntelliJ 的所有编译错误消息:

Error:(31, 40) inferred type arguments [Nothing,name.abhijitsarkar.scala.MyList[Int]] do not conform to class MyListMatcher's type parameter bounds [Int,A <: name.abhijitsarkar.scala.MyList[Int]]
  def equal[Int](right: MyList[Int]) = new MyListMatcher(right)
                                       ^
Error:(31, 58) type mismatch;
 found   : name.abhijitsarkar.scala.MyList[Int]
 required: A
  def equal[Int](right: MyList[Int]) = new MyListMatcher(right)
                                                         ^
Error:(27, 9) overloaded method value should with alternatives:
  (endWithWord: org.scalatest.words.EndWithWord)(implicit ev: <:<[name.abhijitsarkar.scala.MyList[scala.Int],String])MyListSpec.this.ResultOfEndWithWordForString <and>
  (startWithWord: org.scalatest.words.StartWithWord)(implicit ev: <:<[name.abhijitsarkar.scala.MyList[scala.Int],String])MyListSpec.this.ResultOfStartWithWordForString <and>
  (includeWord: org.scalatest.words.IncludeWord)(implicit ev: <:<[name.abhijitsarkar.scala.MyList[scala.Int],String])MyListSpec.this.ResultOfIncludeWordForString <and>
  (notExist: org.scalatest.words.ResultOfNotExist)(implicit existence: org.scalatest.enablers.Existence[name.abhijitsarkar.scala.MyList[scala.Int]])Unit <and>
  (existWord: org.scalatest.words.ExistWord)(implicit existence: org.scalatest.enablers.Existence[name.abhijitsarkar.scala.MyList[scala.Int]])Unit <and>
  (containWord: org.scalatest.words.ContainWord)org.scalatest.words.ResultOfContainWord[name.abhijitsarkar.scala.MyList[scala.Int]] <and>
  (haveWord: org.scalatest.words.HaveWord)MyListSpec.this.ResultOfHaveWordForExtent[name.abhijitsarkar.scala.MyList[scala.Int]] <and>
  (beWord: org.scalatest.words.BeWord)MyListSpec.this.ResultOfBeWordForAny[name.abhijitsarkar.scala.MyList[scala.Int]] <and>
  (inv: org.scalactic.TripleEqualsSupport.TripleEqualsInvocationOnSpread[name.abhijitsarkar.scala.MyList[scala.Int]])(implicit ev: Numeric[name.abhijitsarkar.scala.MyList[scala.Int]])Unit <and>
  [U](inv: org.scalactic.TripleEqualsSupport.TripleEqualsInvocation[U])(implicit constraint: org.scalactic.Constraint[name.abhijitsarkar.scala.MyList[scala.Int],U])Unit <and>
  (notWord: org.scalatest.words.NotWord)org.scalatest.words.ResultOfNotWordForAny[name.abhijitsarkar.scala.MyList[scala.Int]] <and>
  [TYPECLASS1[_], TYPECLASS2[_]](rightMatcherFactory2: org.scalatest.matchers.MatcherFactory2[name.abhijitsarkar.scala.MyList[scala.Int],TYPECLASS1,TYPECLASS2])(implicit typeClass1: TYPECLASS1[name.abhijitsarkar.scala.MyList[scala.Int]], implicit typeClass2: TYPECLASS2[name.abhijitsarkar.scala.MyList[scala.Int]])Unit <and>
  [TYPECLASS1[_]](rightMatcherFactory1: org.scalatest.matchers.MatcherFactory1[name.abhijitsarkar.scala.MyList[scala.Int],TYPECLASS1])(implicit typeClass1: TYPECLASS1[name.abhijitsarkar.scala.MyList[scala.Int]])Unit <and>
  (rightMatcherX1: org.scalatest.matchers.Matcher[name.abhijitsarkar.scala.MyList[scala.Int]])Unit
 cannot be applied to (name.abhijitsarkar.scala.MyListMatcher[Int(in class MyListMatcher),A])
      o should equal(op)
        ^
Error:(46, 21) name.abhijitsarkar.scala.Cons[Int] does not take parameters
      case Cons[Int](h, t) if right.isInstanceOf[Cons[Int]] => {
                ^

回答我自己的问题,下面的测试代码有效。问题是在方法 isEqual.

中不包含 Int 类型参数和 Cons 大小写
class MyListSpec extends FlatSpec with Matchers {
  "MyList" should "be instantiated as expected" in {
    val inputAndOutput = Table(
      ("ip", "op"),
      (Nil, MyNil[Int]),
      (List(1), Cons[Int](1, MyNil[Int])),
      (List(1, 2), Cons[Int](1, Cons[Int](2, MyNil[Int]))),
      (List(1, 2, 3), Cons[Int](1, Cons[Int](2, Cons[Int](3, MyNil[Int]))))
    )

    forAll(inputAndOutput) { (ip, op) =>
      val o = MyList(ip: _ *)
      println(o)

      o should equal(op)
    }
  }

  def equal(right: MyList[Int]) = new MyListMatcher(right)
}

class MyListMatcher(val right: MyList[Int]) extends Matcher[MyList[Int]] {
  override def apply(left: MyList[Int]): MatchResult = {
    MatchResult(
      isEqual(left, right),
      s"""MyList $left did not match "$right"""",
      s"""MyList $left matched "$right""""
    )
  }

  def isEqual(left: MyList[Int], right: MyList[Int]): Boolean = {
    left match {
      case _: MyNil[Int] if right.isInstanceOf[MyNil[Int]] => println("Both are Nil."); true
      case Cons(h, t) if right.isInstanceOf[Cons[Int]] => {
        println("Both are Cons.")
        val r = right.asInstanceOf[Cons[Int]]

        h == r.h && isEqual(t, r.t)
      }
      case _ => println("No match."); false
    }
  }
}

你的代码的问题是这里的这一行:

def equal[Int](right: MyList[Int]) = new MyListMatcher(right)

TL;DR

改变这个:

class MyListMatcher[Int, A <: MyList[Int]](val right: A) extends Matcher[A] {

为此:

class MyListMatcher[A <: MyList[Int]](val right: A) extends Matcher[A] {

完整说明

您遇到的问题只是 Scala 编译器试图弄清楚您的新 MyListMatcher 实例应该是什么类型参数。

因为你的 MyListMatcher class 是一个带有两个参数的类型构造函数(一个被硬编码为 Int ,另一个是从构造函数参数推断出来的),Scala 编译器看到了这个并且假设第一个参数是 Nothing 因为它没有在 equal 方法中指定。

只需删除第一个 Int 类型参数,您的问题就会消失