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

29 апреля 2015

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

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.