创建单一类型对象的列表

问题描述

我有一个 Animal 特征和一些案例类,如下所示

sealed trait Animal

trait Domestic extends Animal
trait Wild extends Animal

case class Dog(id: UUID = UUID.randomUUID()) extends Domestic
case class Lion(id: UUID = UUID.randomUUID()) extends Wild

这是我的 Herd 类,它可以包含单一类型动物的列表

case class Herd[T <: Animal](get: T*)

我想创造的是一群单一类型的动物。

val herd1 = Herd(Cat(),Cat())
val herd2 = Herd(Cat(),Lion())

在 Scala 中两者都是有效的,但是如果您查看一群猫和狮子的含义,那就没有意义了。有没有办法将 Herd 限制为单一类型?

解决方法

尝试引入两个类型参数 AB,然后将它们与泛化类型约束 A =:= B 相关联

case class Herd[A <: Animal,B](x: A,y: B*)(implicit ev: A =:= B)

Herd(Lion())         // ok
Herd(Lion(),Lion()) // ok
Herd(Cat(),Lion())  // compile-time error

究竟是什么=:=

考虑以下具有两个类型参数 AB 的方法,我们旨在传达它们应该相等或至少 A 应该是 B 的子类型

scala> def f[A,B](a: A,b: B): B = {
     |   val x: B = a
     |   x
     | }
         val x: B = a
                    ^
On line 2: error: type mismatch;
        found   : a.type (with underlying type A)
        required: B

上述定义中两个类型参数完全不相关,方法体不能影响类型参数的类型推断,所以会出错。现在让我们尝试将它们与类型绑定 A <: B

scala> def f[A <: B,b: B): B = {
     |   val x: B = a
     |   x
     | }
def f[A <: B,b: B): B

所以这可以编译,但是编译器总是会通过计算给定参数的最小上限来尝试满足类型边界

scala> f(Lion(),Dog())
val res32: Product with Animal with java.io.Serializable = Lion(...)

我们需要更多的东西来解决编译器推导出最小上限的倾向,这就是广义类型相等约束发挥作用的地方

scala> def f[A <: B,b: B)(implicit ev: A =:= B): B = {
     |   val x: B = a
     |   x
     | }
def f[A <: B,b: B)(implicit ev: A =:= B): B

scala> f(Lion(),Cat())
        ^
       error: Cannot prove that Lion =:= Product with Animal with java.io.Serializable.

现在编译器仍然必须尝试生成给定参数的最小上限,但是它还必须满足能够为两种类型 ev 和 { 生成见证 A 的额外要求{1}} 相等。 (请注意,如果可能,编译器会自动实例化见证 B。)

一旦我们有了见证人 ev,我们就可以通过它的 ev 方法在类型 AB 之间自由移动,例如,考虑

apply

注意 scala> type A = Lion type A scala> type B = Lion type B scala> val a: A = Lion() val a: A = Lion(...) scala> val ev: =:=[A,B] = implicitly[A =:= B] val ev: A =:= B = generalized constraint scala> ev.apply(a) val res44: B = Lion(...) 如何键入 ev.apply(a)。我们可以这样应用B的原因是因为它实际上是一个函数

=:=

所以隐式参数列表

scala> implicitly[(A =:= B) <:< Function1[A,B]]
val res43: A =:= B <:< A => B = generalized constraint

实际上指定了一个隐式转换函数

(implicit ev: A =:= B)

所以现在编译器能够在任何需要的地方自动注入隐式转换,所以以下

(implicit ev: A => B)

自动扩展为

def f[A <: B,b: B)(implicit ev: A =:= B): B = {
  val x: B = a
  x
}

总而言之,就像类型边界一样,广义类型约束是要求编译器在编译时对我们的代码库进行进一步检查的另一种方式。