简体   繁体   中英

Treating a covariant type as invariant in Scala?

I have a scenario where I'm trying to pattern-match against a case-class in a way that allows type inference to infer the type of the second argument from the first.

This works correctly when my types are invariant, but when one of the types is covariant Scala (correctly) is unable to infer the type of the second argument. I can work around this by using casting, but I'd like to aim for a type safe solution.

This is probably best explained with code, so I'll start with a very simplified example of what I'm doing. This particular example below does work - the issueis elaborated on below it.

// 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"))))

In this example, since the type argument T of Schema is invariant. When destructuring the "Changeset" class, it's safe to infer that the second argument is T - either Contact or Account in this example (And the Scala compiler correctly does this)

However in the codebase I'm working with, this parameter is covariant, with a non-trivial amount of work to change this.

trait Schema[+T]

This would mean that, as far as type safety is concerned, we cannot guarantee that Changeset( Contacts, _ ) has a type parameter of 'Contact', since we could also have Changeset[Any]( Contacts, Seq[Potato] ) .

At runtime this assertion always holds true, but the compiler obviously can't guarantee this.

I have a plan to refactor some of the legacy code to make this possible, but it's a fair bit of work. Before I dive too far down that rabbit hole, I wanted to double check there wasn't a simpler way to do this.

The types of T will always be a leaf with no subclasses, and I'm able to give T a type bounds if needed. Given those constraints it seems it would be possible for a language to correctly infer the types while pattern matching, but I'm unsure if Scala specifically can do this.

You can introduce another trait, an invariant one:

trait LeafSchema[T] extends Schema[T]

which Contact and Account extend. Then you insist on taking a LeafSchema in anything that needs a safe match, and get to it using a match on Schema .

Whether this is actually wise, or a hole in the type system, I am not sure. I am inclined to view it as a hole. But you can do it, and in your case it ought to be safe.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM