Reputation: 6272
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
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 TypeTag
s.
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