When you use subtyping, once you go "up", the only idiomatic way to go "down" is pattern matching.
For example,
val apple: Apple = new Apple()
val fruit: Fruit = apply
fruit match {
case Apple() => // ...
case _ => // ...
}
However, due to type erasure, this is not a good solution for your problem.
First solution
If you are willing to abandon subtyping altogether, you can use type classes which can solve this problem quite elegantly:
trait Foo[A, F[_]] {
def skipRoot(path: F[A]): F[A]
}
implicit def fooList[A] = new Foo[A, List] {
def skipRoot(path: List[A]): List[A] = path.tail
}
implicit def fooVector[A] = new Foo[A, Vector] {
def skipRoot(path: Vector[A]): Vector[A] = path.tail
}
def genericSkipRoot[A, F[_]](collection: F[A])(implicit ev: Foo[A, F]) =
implicitly[Foo[A, F]].skipRoot(collection)
genericSkipRoot(List(1, 2, 3)) // List(2, 3)
genericSkipRoot(Vector(1, 2, 3)) // Vector(2, 3)
Sure, you have to define explicit typeclass instances for all types that you want to use, but you gain a lot of flexibility, plus it's idiomatic. And you can even define instances for collections from other libraries that don't happen to extend Seq
.
Second solution
If you want to keep subtyping, you need to use Scala 2.13 in which the collections library went through some redesign.
There, you will find that there's a trait
trait IterableOps[+A, +CC[_], +C]
which, as you can see, retains information about the collection. Your method then becomes:
def foo[S <: immutable.SeqOps[_, Seq, S]](sa: S): S = sa.tail
foo(List(1, 2, 3)) // List(2, 3)
foo(Vector(1, 2, 3)) // Vector(2, 3)
(note that you need to avoid the A
parameter in order to make it compile, because you need to return just S
and not S[A]
)