Generic function for Scala sequences like `Seq`, `IndexedSeq`, `List`

Viewed 246

I have a function which wraps around Seq.tail. I would like the function to return List for List, IndexedSeq for IndexedSeq, Seq for Seq.

Currently I use asInstanceOf to work around the fact tail always returns Seq to me.

    def skipRoot[S <: Seq[String]](path: S): S = {
      assert(path.head.nonEmpty)
      path.tail.asInstanceOf[S] // a nicer generic solution?
    }

    val list: List[String] = skipRoot("a" :: Nil)

I guess there must be a nicer way, perhaps using higher kinded types?

3 Answers

You use only head and tail methods. Both are declared in scala.collection.IterableOps, so why not use it?

def skipRoot[S <: IterableOps[PathItem]](path: S): S = { ... }

EDIT for Scala 2.12:

Try this:

skipRoot[S <: GenTraversableLike[String, S]](path: S): S = {...}

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])

Try

def skipRoot[S[X] <: IterableOps[X, Seq, S[X]]](path: S[String]): S[String] = {
  assert(path.head.nonEmpty)
  path.tail
}

Or Scala 2.12:

def skipRoot[S[X] <: GenTraversableLike[X, S[X]]](path: S[String]): S[String] = {
  assert(path.head.nonEmpty)
  path.tail
}

In Scala 3 you can also have a signature

def skipRoot[S[X] <: Seq[X] { def tail: S[X] }](path: S[String]): S[String] 

https://scastie.scala-lang.org/DmytroMitin/vA42tuUlRJSvTHAKiPzzbQ/3

Related