简体   繁体   中英

Covariant safe cast in Scala

I'm trying to write a safe cast for a class with a covariant type parameter like:

case class Foo[+A](a: A) {
  def safeCast[B <: A](): Option[B] = ???
}

It's being populated by a dynamic external data source, but I at least want to ensure statically that B is a subclass of A , then return a None if the typecast fails. I keep getting errors about A being in a covariant position. I can get it to typecheck by leaving off the <: A , but how can I specify this static guarantee about B ?

I know it is preventing situations like assigning a Foo[Dog] to a Foo[Animal] val, then trying to do something like safeCast[Mammal] where Mammal isn't a subtype of Dog . That's actually okay in my case. Even allowing casting to a supertype of Animal would be okay. I mostly want to statically prevent someone trying a safeCast[Plant] .

Note, I can get it to typecheck with a function external to the class like the following, but I'm wanting a method on the class.

def safeCast[A, B <: A](foo: Foo[A]): Option[B] = ???

As a bonus, if you know of a way to implement this using cats or something without isInstanceOf , that would be very useful.

What about a typeclass approach.

import scala.reflect.ClassTag

@annotation.implicitNotFound("${B} is not a subtype of ${A}")
sealed trait Caster[A, B] {
  def safeCast(a: A): Option[B]
}

object Caster {
  implicit def subtypeCaster[A, B](implicit ev: B <:< A, ct: ClassTag[B]): Caster[A, B] =
    new Caster[A, B] {
      override final def safeCast(a: A): Option[B] =
        ct.unapply(a)
    }
}

Which can be used like this:

sealed trait Animal
final case class Dog(name: String) extends Animal
final case class Cat(name: String) extends Animal

final case class Foo[+A](a: A)

implicit class FooOps[A] (private val foo: Foo[A]) extends AnyVal {
  @inline
  final def safeCastAs[B](implicit caster: Caster[A, B]): Option[Foo[B]] =
    caster.safeCast(foo.a).map(b => Foo(b))
}

val animal: Foo[Animal] = Foo(Dog("luis"))

animal.safeCastAs[Dog] 
// res: Option[Foo[Dog]] = Some(Foo(Dog("luis")))

animal.safeCastAs[Cat] 
// res: Option[Foo[Cat]] = None

animal.safeCastAs[String] 
// compile error: String is not a subtype of Animal

Two important notes:

  • The trick here is to define the method outside the class as an extension method.
    (at that point one may even consider dropping the typeclass altogether and just use the implicit evidence and the classtag themselves on the extension method) .

  • Due to the use of classtag, you should take care that both A & B are just plain types. If they are higher kinded types like List , you may end up with runtime errors.

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