Перестаем бояться тайптэгов (TypeTags) в Scala
29 апреля 2015
Как вы можете помнить, в мире Java есть такая штука под названием type erasure. Мы сталкивались с ней, когда в первый раз пробовали работать с JSON в Scala. Суть проблемы заключается в том, что во время выполнения программы нельзя узнать точный тип генериков, так как информация о типах, которыми они были параметризованы, теряется на этапе компиляции. Вызвано это в основном историческими причинами, так как генерики появились только в Java 1.5, но в какой-то степени, наверное, это также можно считать и оптимизацией. К счастью, в Scala это ограничение можно легко обойти, воспользовавшись TypeTags.
Примечание: Если вам доводилось слышать про манифесты в Scala, знайте, что это своего рода тайптэги предыдущего поколения. Там все почти то же самое, только иногда хуже. В настоящее время манифесты считаются устаревшими и в ближайших версиях Scala их, скорее всего, уже не будет. Вместо манифестов всегда следует использовать тайптэги.
Рассмотрим пример:
def processMap(json: Map[String, Any]) {
json match {
case _ : Map[String, String] =>
println("Map of strings")
case _ : Map[String, List[String]] =>
println("Map of list of strings")
}
}
processMap(Map("aaa" -> "bbb"))
processMap(Map("ccc" -> List("ddd", "eee")))
Запустив этот скрипт, вы увидите что-то вроде:
tion.immutable.Map[String,String] (the underlying of Map[String,String]
) is unchecked since it is eliminated by erasure
case _ : Map[String, String] =>
^
warning: non-variable type argument List[String] in type pattern scala.
collection.immutable.Map[String,List[String]] (the underlying of Map[St
ring,List[String]]) is unchecked since it is eliminated by erasure
case _ : Map[String, List[String]] =>
^
warning: unreachable code
println("Map of list of strings")
^
three warnings found
Map of strings
Map of strings
Очевидно, программа работает неверно. Но исправить ситуацию можно очень просто, переписав скрипт таким образом:
import scala.reflect.runtime.universe._
def processMap[T: TypeTag](json: Map[String, T]) {
typeOf[T] match {
case t if t =:= typeOf[String] =>
println("Map of strings")
case t if t =:= typeOf[List[String]] =>
println("Map of list of strings")
}
}
processMap(Map("aaa" -> "bbb"))
processMap(Map("ccc" -> List("ddd", "eee")))
Запускаем, и в этот раз видим правильный результат:
Map of list of strings
Подробное объяснение того, как это работает, можно найти в документации по Scala. Если в двух словах, то код:
… полностью эквивалентен:
Класс TypeTag[T]
предназначен для хранения полной информации о типе T
. Это как раз та информация, которую мы теряем на этапе компиляции. Более того, это в точности тот же самый класс, с которым работает сама Scala во время компиляции наших программ. Получить тайптэг конкретного типа можно при помощи метода typeTag[T]
. Если Scala видит метод с неявно передаваемым тайптэгом и в текущем контексте нет подходящего неявного значения, значение аргумента генерируется автоматически. Что же до метода:
… то это просто другой способ обратиться к tag.tpe
. Для определения типа следует сравнивать именно значения типа Type
, а не TypeTag[T]
, притом делать это при помощи специальных операторов =:=
и <:<
. Кстати, давайте рассмотрим пример использования последнего. Как несложно догадаться, он нужен для случая с подтипами:
class Foo
class Bar extends Foo
object Main extends App {
def processMap[T: TypeTag](json: Map[String, T]) {
typeOf[T] match {
case t if t =:= typeOf[String] =>
println("Map of strings")
case t if t <:< typeOf[List[Foo]] =>
println("Map of list of foos")
}
}
processMap(Map("aaa" -> "bbb"))
processMap(Map("ccc" -> List(new Bar)))
}
Компилируем и запускаем:
$ scala -cp . Main
Map of strings
Map of list of foos
Заметьте, что здесь мы честно компилируем программу, а не запускаем ее в интерпретаторе. В последнем случае была бы получена ошибка No TypeTag available for ...
. Объяснение, почему так происходит, можно найти здесь.
Следует отметить, что помимо TypeTag[T]
в Scala есть еще ClassTag[T]
и WeakTypeTag[T]
. Подробности о них вы найдете в уже упомянутой документации.
Как видите, проблема type erasure решена в Scala довольно просто, и в какой-то степени даже элегантно. Следует однако отметить, что прибегать к такого рода рефлексии приходится очень редко и обычно это является code smell. Если повезет, полученные из этой заметки знания вам никогда не пригодятся.
Метки: Scala, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.