Scala case 类 可以在继承函数和非继承函数中匹配吗?

Can Scala case classes match in both an inherited and non-inherited function?

通过 Essential Scala 这本书,我发现自己在玩链表示例。我遇到了这样一种情况,我有一个超类,用于任何列表,还有一个子类,用于 Ints 的列表。我可以询问任一类型列表的长度,以及整数列表的总和。但是,我在使用案例 类 建模时遇到了一些麻烦。自然继承结构如下:

         List
     ↙    ↓    ↘
Pair   IntList   End
 ↓    ↙       ↘   ↓
IntPair       IntEnd

在 List 中,我希望能够在 List 中匹配 Pair VS End,这可以通过 Pair and End case 类 来实现.我在这样的求和函数中使用它:

  final def length(counter: Int=0): Int =
    this match {
      case Pair(_, tail:List[A]) => tail.length(counter+1)
      case End() => counter

但是,我也希望能够在 IntList 中匹配 IntPair VS IntEnd。 IntList 中的自然实现如下所示:

  final def sum(counter: Int=0): Int =
    this match {
      case IntPair(head, tail) => tail.sum(head + counter)
      case IntEnd() => counter

但是,因为 Pair 已经是一个 case 对象,所以 IntPair 不能也是一个。但如果不是,那么我们就无法在 sum 中进行匹配。如果我们不继承,那么 length 将停止工作,因为匹配案例不知道 IntPair/IntEnd.

我做了一个 Github gist with the full Scala worksheet,如果有帮助的话。它可以是 运行 和 scala -nc inheritance.sc.

我应该采用什么方法来代替这个方法?

您可以定义自己的提取器而不是使用 case 类。这就是您可以同时使用匹配和继承的方法。

要定义提取器,您必须在对象中指定 unapply 方法,就像那样;

object IntPair {
  def unapply(intPair: IntPair): Option[(Int, Int)] = Some(intPair._1, IntPair._2)
}

了解更多信息: https://docs.scala-lang.org/tour/extractor-objects.html

有几种方法可以解决这个问题。这完全取决于您为了实现目标愿意在哪些方面做出妥协。

如果您希望 IntPair 同时具有 .length.sum,那么一种解决方案是继承这两个特征。

case class IntPair(...) extends Pair(head,tail) with IntList

但这不起作用,因为不允许 case classcase class 继承。

所以为了继承Pair它不能是caseclass,但是我们不必放弃[=的所有好的特性17=],我们只需要将它们编码在.

sealed class End[A]() extends List[A]
object End {
  def apply[A](): End[A] = new End()
  def unapply[A](arg: End[A]): Boolean = true
}
sealed class Pair[A](val head: A, val tail: List[A]) extends List[A]
object Pair {
  def apply[A](head: A, tail: List[A]): Pair[A] = new Pair(head, tail)
  def unapply[A](arg: Pair[A]): Option[(A, List[A])] =
    Some((arg.head, arg.tail))
}

有了它,我们就可以创建所需的继承。

final case class IntEnd() extends End[Int]() with IntList
final case class IntPair(override val head: Int
                        ,override val tail: IntList
                        ) extends Pair(head,tail) with IntList

现在测试断言将通过。

val example = IntPair(1, IntPair(2, IntPair(3, IntEnd())))
assert(example.sum() == 6)
assert(example.tail.sum() == 5)
assert(IntEnd().sum() == 0)
assert(example.length() == 3)
assert(IntEnd().length() == 0)

这个问题实际上很好地说明了为什么传统的 sub-type 多态性 (通常在 OOP 语言上建模思想继承) 不足以自然地实现一些用例。

所以让我们看看其他选择。在这种情况下,我将介绍:隐含证据扩展方法类型类 (这就像前两者的更强大和灵活的组合).

首先让我们定义一个通用的定义a List.

sealed trait MyList[+A] extends Product with Serializable {
  final def :!:[B >: A](b: B): MyList[B] = new :!:(b, this)

  final def length: Int = {
    @annotation.tailrec
    def loop(remaining: MyList[A], acc: Int): Int =
      remaining match {
        case _ :!: tail => loop(remaining = tail, acc + 1)
        case MyNil => acc
      }
    loop(remaining = this, acc = 0)
  }
}
final case class :!:[+A](head: A, tail: MyList[A]) extends MyList[A]
final case object MyNil extends MyList[Nothing]

隐含证据。

思路很简单,一个List提供一个sum方法,只要它的元素是Ints.
这是直觉,我们可以对其进行编码:

sealed trait MyList[+A] extends Product with Serializable {
  // ...

  // B required due variance.
  final def sum[B >: A](implicit ev: B =:= Int): Int = {
    @annotation.tailrec
    def loop(remaining: MyList[B], acc: Int): Int =
      remaining match {
        case i :!: tail => loop(remaining = tail, acc + i)
        case MyNil => acc
      }
    loop(remaining = this, acc = 0)
  }
}

这种编码允许我们在任何 List 上调用 sum,只要编译器可以证明 List[=114] 的元素=] 是 Ints,否则该方法将不可用。

扩展方法。

我们可以通过添加 sum 作为 扩展方法 实现与 隐含证据 完全相同的行为 仅列表,共整数
如果您不控制数据类型,或者即使您控制了数据类型,但您有很多方法需要一些条件,所以您可以将代码拆分为多个文件,甚至允许用户 opt-in对于这些扩展。

implicit class IntListOps(private val intList: MyList[Int]) extends AnyVal {
  def sum: Int = {
    @annotation.tailrec
    def loop(remaining: MyList[Int], acc: Int): Int =
      remaining match {
        case i :!: tail => loop(remaining = tail, acc + i)
        case MyNil => acc
      }
    loop(remaining = intList, acc = 0)
  }
}

就像以前一样,这种编码允许我们调用 sum 方法到任何 List 中,只要编译器可以证明 [=77] 的元素=]ListInts (并且扩展方法在范围内,通常通过 import

类型类

最后,函数式编程语言最强大的模式之一,typeclasses

在这种情况下,我们需要添加很多“样板”代码,以及使用扩展方法。但是,必须注意的是,这允许我们扩展此 sum 方法以处理可求和的蚂蚁类型,例如 DoubleBigInt.

trait Summable[A] {
  def sum(a1: A, a2: A): A
  def zero: A
}

object Summable {
  implicit final val IntSummable: Summable[Int] =
    new Summable[Int] {
      override def sum(i1: Int, i2: Int): Int = i1 + i2
      override val zero: Int = 0
    }
  
  object syntax {
    implicit class SummableOps[A](private val a1: A) extends AnyVal {
      @inline final def |+|(a2: A)(implicit ev: Summable[A]): A = ev.sum(a1, a2)
    }
    
    implicit class SummableListOps[A](private val list: MyList[A]) extends AnyVal {
      @inline final def sum(implicit ev: Summable[A]): A = {
        @annotation.tailrec
        def loop(remaining: MyList[A], acc: A): A =
          remaining match {
            case i :!: tail => loop(remaining = tail, acc |+| i)
            case MyNil => acc
          }
        loop(remaining = list, acc = ev.zero)
      }
    }
  }
}

同样,编译器将允许我们在 List 上调用 sum,只要它能证明它们的元素是可求和的。而且,由于 implicit scope 非常灵活,您可以轻松创建自定义类型 Summable.

此外,我们甚至可以进一步概括这一点,如果我们在任何可以迭代的类型上提供 sum 呢?
这正是 cats 所做的,它提供了 (在另一个非常有用的东西之间) 两个抽象 Monoid (这是 SummableFoldable (表示迭代某些东西的可能性) 的数学名称,它结合起来给我们 combineAll 方法 (以及其他强大的抽象,如 foldMap 在任何 “集合”“可组合” 个元素。


虽然这些技术中的每一种都非常强大,但 Scala 的真正强大之处在于它允许您混合使用它们以获得您想要的 API。
例如,std List 使用 隐式证据 提供 sum 方法,但存在 类型类.

此外,还有其他类型的多态技术,如 duck typing (结构类型),我个人不喜欢,但它是有助于了解它。
如果您有兴趣,我曾 wrote 介绍过其中一些技术及其在 Scala 中的差异。


可以看到完整代码here