在 Scala 中将协变类型视为不变类型?

Treating a covariant type as invariant in Scala?

我有一个场景,我试图以一种允许类型推断从第一个参数推断第二个参数类型的方式对 case-class 进行模式匹配。

当我的类型是不变的,但当其中一种类型是协变时,Scala 无法(正确地)推断出第二个参数的类型。我可以通过使用强制转换来解决这个问题,但我想要一个类型安全的解决方案。

这可能最好用代码来解释,所以我将从我正在做的一个非常简单的例子开始。下面的这个特定示例 确实有效 - 下面详细说明了这个问题。

// Schemas are an `open` type, so I don't know all possibilities ahead of time
trait Schema[T]
case object Contacts extends Schema[Contact]
case object Accounts extends Schema[Account]

case class Contact( firstName:String, lastName:String )
case class Account( name:String )

// I only really need records here. Schema does contain some
// needed metadata, but in this case is mostly just used 
// as a type-tag so we know what type of record we have.
case class Changeset[T]( schema:Schema[T], records:Seq[T] )

def processChangeset[T]( cs: Changeset[T] ):Unit = {
    val names = cs match {
        // This is the important bit - inferring that
        // `contacts` is a Seq[Contact]` and so forth.
        case Changeset( Contacts, contacts ) => contacts.map(_.firstName)
        case Changeset( Accounts, accounts ) => accounts.map(_.name)
        case _ => Nil
    }        
}

processChangeset( Changeset( Accounts, Seq(Account("ACME"))))

在这个例子中,由于Schema的类型参数T是不变的。当解构 "Changeset" class 时,可以安全地推断出第二个参数是 T - 在这个例子中要么是 Contact 要么是 Account(Scala 编译器正确地做到了这一点)

但是在我使用的代码库中,这个参数是协变的,需要大量的工作来改变它。

trait Schema[+T]

这意味着,就类型安全而言,我们不能保证 Changeset( Contacts, _ ) 的类型参数为 'Contact',因为我们也可以有 Changeset[Any]( Contacts, Seq[Potato] )

在运行时这个断言总是成立的,但编译器显然不能保证这一点。

我计划重构一些遗留代码以实现这一点,但这是一项相当大的工作量。在我深入那个兔子洞之前,我想仔细检查是否有更简单的方法来做到这一点。

T 的类型将始终是没有子class 的叶子,如果需要,我可以给 T 一个类型边界。鉴于这些限制,一种语言似乎有可能在模式匹配时正确推断类型,但我不确定 Scala 是否特别可以做到这一点。

你可以引入另一个特征,一个不变的特征:

trait LeafSchema[T] extends Schema[T]

其中 ContactAccount 扩展。然后你坚持在任何需要安全匹配的地方进行 LeafSchema,然后使用 Schema.

上的匹配来实现它

这到底是明智的,还是类型系统中的漏洞,我不确定。我倾向于将其视为一个洞。但是你可以做到,而且在你的情况下它应该是安全的。