← На главную

Перестаем бояться тайптэгов (TypeTags) в Scala

Как вы можете помнить, в мире Java есть такая штука под названием type erasure. Мы сталкивались с ней, когда в первый раз пробовали работать с JSON в Scala. Суть проблемы заключается в том, что во время выполнения программы нельзя узнать точный тип генериков, так как информация о типах, которыми они были параметризованы, теряется на этапе компиляции. Вызвано это в основном историческими причинами, так как генерики появились только в Java 1.5, но в какой-то степени, наверное, это также можно считать и оптимизацией. К счастью, в Scala это ограничение можно легко обойти, воспользовавшись TypeTags.

Примечание: Если вам доводилось слышать про манифесты в Scala, знайте, что это своего рода тайптэги предыдущего поколения. Там все почти то же самое, только иногда хуже. В настоящее время манифесты считаются устаревшими и в ближайших версиях Scala их, скорее всего, уже не будет. Вместо манифестов всегда следует использовать тайптэги.

Рассмотрим пример:

#!/usr/bin/env 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")))

Запустив этот скрипт, вы увидите что-то вроде:

warning: non-variable type argument String in type pattern scala.collec 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

Очевидно, программа работает неверно. Но исправить ситуацию можно очень просто, переписав скрипт таким образом:

#!/usr/bin/env scala 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 strings Map of list of strings

Подробное объяснение того, как это работает, можно найти в документации по Scala. Если в двух словах, то код:

def processMap[T: TypeTag](json: Map[String, T])

… полностью эквивалентен:

def processMap[T](json: Map[String, T])(implicit tag: TypeTag[T])

Класс TypeTag[T] предназначен для хранения полной информации о типе T. Это как раз та информация, которую мы теряем на этапе компиляции. Более того, это в точности тот же самый класс, с которым работает сама Scala во время компиляции наших программ. Получить тайптэг конкретного типа можно при помощи метода typeTag[T]. Если Scala видит метод с неявно передаваемым тайптэгом и в текущем контексте нет подходящего неявного значения, значение аргумента генерируется автоматически. Что же до метода:

def typeOf[T](implicit tag: TypeTag[T]): Type

… то это просто другой способ обратиться к tag.tpe. Для определения типа следует сравнивать именно значения типа Type, а не TypeTag[T], притом делать это при помощи специальных операторов =:= и <:<. Кстати, давайте рассмотрим пример использования последнего. Как несложно догадаться, он нужен для случая с подтипами:

import scala.reflect.runtime.universe._ 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))) }

Компилируем и запускаем:

$ scalac typetags2.scala $ 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. Если повезет, полученные из этой заметки знания вам никогда не пригодятся.