Martin Eden
Martin Eden

Reputation: 6272

Generic strongly-typed scala method to retrieve item of a particular type from a collection

Imagine I have this class:

class Example {
    val list = List(new Apple(), new Orange(), Banana());
    def getIfPresent[T <: Fruit] : Option[T] = list.collectFirst { case x : T => x }
}

You use it like this:

val example = new Example();
match example.getIfPresent[Apple] {
    case Some(apple) => apple.someAppleSpecificMethod();
    case None => println("No apple");
}

Now, of course, this doesn't work in the JVM, because of type erasure. getIfPresent just matches on the type Fruit in the collectFirst partial function, instead of the actual type specified in the call.

I have tried to get my head around type tags and class tags, and really have no idea how I would implement the above method. The examples that I see are trying to do very different things. How could I achieve a method that does what I want, either with TypeTags or some other mechanism I'm unaware of?


Edit: m-z's answer below is the full solution, but here is how it looks with my example code:

class Example {
    val list = List(new Apple(), new Orange(), Banana());
    def getIfPresent[T <: Fruit : ClassTag] : Option[T] = list.collectFirst { case x : T => x }
}

Just needed to add : ClassTag!

Upvotes: 2

Views: 177

Answers (1)

Michael Zajac
Michael Zajac

Reputation: 55569

You can do this using ClassTag, to an extent.

import scala.reflect.ClassTag

// Modify to apply whatever type bounds you find necessary
// Requires Scala ~2.11.5 or greater (not sure of the exact version, but 2.11.1 does not work, and 2.11.5 does)
def findFirst[A : ClassTag](list: List[Any]): Option[A] =
    list collectFirst { case a: A => a }

val l = List(1, "a", false, List(1, 2, 3), List("a", "b"))

scala> findFirst[Boolean](l)
res22: Option[Boolean] = Some(false)

scala> findFirst[Long](l)
res23: Option[Long] = None

But there are some caveats with ClassTag, in that it will only match the class, and not the type:

scala> findFirst[List[String]](l)
res24: Option[List[String]] = Some(List(1, 2, 3)) // No!

You can use a TypeTag to get around this, but it won't work with a List[Any]. Here is one possible (sort of ugly) trick:

import scala.reflect.runtime.universe.{typeOf, TypeTag}

case class Tagged[A : TypeTag](a: A) {
    def tpe = typeOf[A]
}

implicit class AnyTagged[A : TypeTag](a: A) {
    def tag = Tagged(a)
}

def findFirst[A : TypeTag](list: List[Tagged[_]]): Option[A] =
    list collectFirst { case tag @ Tagged(a) if(tag.tpe =:= typeOf[A]) => a.asInstanceOf[A] }

The only way I can think of to hold onto the TypeTag of each element is to literally hold onto it with a wrapper class. So I have to construct the list like this:

val l = List(1.tag, "a".tag, false.tag, List(1, 2, 3).tag, List("a", "b").tag)

But it works:

scala> findFirst[List[String]](l)
res26: Option[List[String]] = Some(List(a, b))

There may be a more elegant way to construct such a list with TypeTags.


For fun, you can also try to do this with shapeless using an HList and select. The difference is that instead of returning Option[A], select will return A (the type you want), but if the HList contains no A, it won't compile.

import shapeless._

val l = 1 :: "a" :: false :: List(1, 2, 3) :: List("a", "b") :: HNil

scala> l.select[Boolean]
res0: Boolean = false

scala> l.select[Boolean]
res1: Boolean = false

scala> l.select[List[String]]
res2: List[String] = List(a, b)

scala> l.select[Long]
<console>:12: error: Implicit not found: shapeless.Ops.Selector[shapeless.::[Int,shapeless.::[String,shapeless.::[Boolean,shapeless.::[List[Int],shapeless.::[List[String],shapeless.HNil]]]]], Long]. You requested an element of type Long, but there is none in the HList shapeless.::[Int,shapeless.::[String,shapeless.::[Boolean,shapeless.::[List[Int],shapeless.::[List[String],shapeless.HNil]]]]].
              l.select[Long]
                      ^

Upvotes: 6

Related Questions