Январь 22, 2017
Теги типов в Scala (TypeTags)

Во всех JVM-языках существует понятие type erasure. Это означает, что во время исполнения у объекта доступна не вся та информация о его типе, какая была доступна компилятору во время компиляции. Из-за того, что JVM ничего не знает о generics, компилятор удаляет всю "generic" информацию типа после компиляции. Например, в runtime невозможно будет определить разницу между List[Int] и List[String].

Для того, чтобы решить эту проблему, в Scala 2.10 появились "теги типов" (ранее были Manifests, которые сейчас являются deprecated). Это некие объекты, которые содержат всю недостающую информацию, которой нет в runtime, но есть во время компиляции.

Есть 3 вида тегов типов:

  • TypeTag;
  • ClassTag;
  • WeakTypeTag.

TypeTag и WeakTypeTag очень похожи, в то время как ClassTag это принципиально другая конструкция.

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


object ListUtils {
  def retrieve[T](list: List[Any]) = 
    list.flatMap {
      case element: T => Some(element)
      case _ => None
    }
}

Из описанного ранее понятно, что такой код работать не будет. Для того чтобы исправить это, можно просто добавить дополнительный неявный параметр:


import scala.reflect.ClassTag

object ListUtils {
  def retrieve[T](list: List[Any])(implicit tag: ClassTag[T]) = 
    list.flatMap {
      case element: T => Some(element)
      case _ => None
    }
}

val list: List[Any] = List(3, 10, "string", List(), "anotherString")
val result = ListUtils.retrieve[String](list)
println(result) // List(string, anotherString)

Для достижения того же самого результата можно также воспользоваться синтаксисом "context bound". Сontext bound - это синтаксический сахар для списка неявных параметров (можно указывать один или несколько типов через ":"). То есть, следующее объявление метода ListUtils.retrieve будет аналогично приведенному выше:


def retrieve[T: ClassTag](list: List[Any]) = 
  list.flatMap {
    case element: T => Some(element)
    case _ => None
}

ClassTag

ClassTag предназначен для хранения частичной информации о Scala-типе. Он содержит информацию только о том классе, чья информация стирается. То есть, предоставляет доступ только к runtime-типу класса. Например, ClassTag[List[String]] будет содержать информацию только о типе List, без подробностей, что это List[String].

Как все это работает? Когда указан неявный параметр с типом ClassTag, компилятор создает его для нас. Вот, что говорится в документации:

If an implicit value of type u.ClassTag[T] is required, the compiler will make one up on demand.

Такой же механизм действует для TypeTag и WeakTypeTag.

Код, приведенный выше, заработает, потому что компилятор перепишет наше условие (так как в области видимости есть неявный тег):


case element: T => Some(element)

На:


case (element @ tag(_: T)) => Some(element)

Конструкция "@" позволяет получить ссылку на объект совпавшего класса. Например:


case Foo(p, q) => // получаем только параметры в переменных p и q
case f @ Foo(p, q) => // получаем весь объект класса Foo через f + параметры в p и q

Если для типа T нет неявного тега ClassTag, то компилятор выдаст предупреждение о том, что наше сопоставление с образцом не будет работать из-за стирания информации о типе T.

Как было упомянуто выше, особенность типа ClassTag заключается в том, что он хранит только частичную информацию и предоставляет доступ только к runtime-типу класса. То есть, мы не сможем отличать типы на более высоких уровнях (можем только на первом уровне). Например, в списке List[List[Any]] мы не сможем отличить List[Int] от List[String]. Для решения данной задачи необходим TypeTag.

TypeTag

TypeTag предназначен для хранения полного описания Scala-типа. С помощью него можно получить информацию о типе любой вложенности - например, как о List[Any], так и о List[Set[Any]].

Для получения полной информации о типе, можно вызвать метод TypeTag.tpe. Рассмотрим пример, из которого видно, что этот метод возвращает в том числе и ту информацию, которой не было в ClassTag:


import scala.reflect.runtime.universe._

object Utils {
  def retrieve[T](x: T)(implicit tag: TypeTag[T]): String =
    tag.tpe match {
      case TypeRef(utype, usymbol, args) =>
        List(utype, usymbol, args).mkString("\n")
    }
}

val list: List[Int] = List(1, 2)
val result = Utils.retrieve(list)
println(result)

// Выведет:
//   scala.type
//   type List
//   List(Int) <- информации об Int в ClassTag нет

Недостаток тега TypeTag заключается в том, что он не может быть использован с объектами в runtime. Если мы заменим в приведенном ранее методе ListUtils.retrieve класс ClassTag на TypeTag, то он перестанет работать. С помощью TypeTag мы можем получить информацию об определенном типе в runtime, но мы не сможем получить с помощью него тип какого-либо объекта в runtime. Например, если мы будем передавать в метод объект List(1, 2) и укажем у этого параметра тип List[Any], то TypeTag покажет, что мы передали List[Any].

Другими словами, TypeTag дает нам информацию о runtime типе, а ClassTag дает нам информацию о том, какой на самом деле тип (верхнеуровневый) у определенного объекта в runtime.

Еще одно небольшое отличие TypeTag от ClassTag заключается в том, что мы должны импортировать его из корректного объекта universe (в случае TypeTag, из scala.reflect.runtime.universe). В то время как ClassTag c universe никак не связан.

Universe provides a complete set of reflection operations which make it possible for one to reflectively inspect Scala type relations, such as membership or subtyping.

WeakTypeTag

TypeTag не позволяет получить тип, если этот тип абстрактный. Для таких задач используется WeakTypeTag[T], который обобщает TypeTag[T]. Он предназначен для хранения описания абстрактных типов. В отличие от TypeTag, компоненты WeakTypeTag могут ссылаться на параметры типа или абстрактные типы.

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


import scala.reflect.runtime.universe._

abstract class MyClass[T] {

  object Utils {
    def retrieve[T](x: T)(implicit tag: WeakTypeTag[T]): String =
      tag.tpe match {
        case TypeRef(utype, usymbol, args) =>
          List(utype, usymbol, args).mkString("\n")
      }
  }

  val list: List[T]
  val result = Utils.retrieve(list)
  println(result)
}

new MyClass[Int] { val list = List(1) }

// Выведет:
//   scala.type
//   type List
//   List(T)

Полученный тип - List[T]. Если бы вместо WeakTypeTag мы использовали TypeTag, компилятор выдал бы сообщение "no TypeTag available for List[T]". Однако, WeakTypeTag старается быть как можно более конкретным: если для абстрактного типа доступен тег типа, WeakTypeTag использует его и сделает тип конкретным, а не абстрактным.

Альтернативный способ получения тегов типов

Наряду с возможностью получить теги типов через неявные параметры, есть возможность воспользоваться для их получения специальными хелперами:


import scala.reflect.classTag
import scala.reflect.runtime.universe.{typeTag, weakTypeTag}

val clsTag = classTag[String]
val typTag = typeTag[List[Int]]
val weakTypTag = weakTypeTag[List[Int]]

Некоторые часто возникающие задачи

Как получить ClassTag, если есть класс (например, полученный через classOf[Int]):


import scala.reflect.ClassTag

val cls = classOf[Int]
val clsTag = ClassTag[Int](cls)

Как получить класс, имея ClassTag:


import scala.reflect.ClassTag

object Utils {
  def retrieveClass[T](a: List[T])(implicit tag: ClassTag[T]): Class[T] = {
    tag.runtimeClass.asInstanceOf[Class[T]]
  }
}

Utils.retrieveClass[Int](List(1, 2)) // вернет Class[Int]

Как получить класс, имея TypeTag:


import scala.reflect.runtime.universe.TypeTag

object Utils {
  def retrieveClass[T](a: List[T])(implicit tag: TypeTag[T]): Class[T] = {
    tag.mirror.runtimeClass(tag.tpe).asInstanceOf[Class[T]]
  }
}

Utils.retrieveClass[Int](List(1, 2)) // вернет Class[Int]

"mirror" - это провайдер информации. Вся информация, предоставленная рефлексией, доступна через "mirrors".

Mirrors are a central part of Scala Reflection. All information provided by reflection is made accessible through Mirrors. Depending on the type of information to be obtained, or the reflective action to be taken, different flavors of mirrors must be used. "Classloader" mirrors can be used to obtain representations of types and members. From a classloader Mirror, it's possible to obtain more specialized "invoker" Mirrors (the most commonly-used mirrors), which implement reflective invocations, such as method/constructor calls and field accesses.

Как получить TypeTag, если есть класс:


import scala.reflect.api
import scala.reflect.runtime.universe._

object Utils {
  def retrieveTag[T](cls: Class[T]): TypeTag[T] = {
    val mirror = runtimeMirror(cls.getClassLoader)
    val typ = mirror.classSymbol(cls).selfType
    TypeTag[T](mirror, new api.TypeCreator {
      def apply[U <: api.Universe with Singleton](m: api.Mirror[U]): U # Type =
        if (m eq mirror) typ.asInstanceOf[U # Type]
        else throw new IllegalArgumentException(s"Type tag defined in $mirror cannot be " +
          s"migrated to other mirrors.")
    })
  }
}

Utils.retrieveTag[Int](classOf[Int]) // вернет TypeTag[Int]

Как сравнить типы, имея TypeTag типа:


import scala.reflect.runtime.universe._

class Foo

class Bar extends Foo

object Utils {
  def retrieveMsg[T: TypeTag](a: List[T]): String = {
    typeOf[T] match {
      case t if t =:= typeOf[String] =>
        "List of strings"
      case t if t <:< typeOf[Foo] =>
        "List of `Foo` inheritor instances"
    }
  }
}

Utils.retrieveMsg[String](List("s1", "s2")) // вернет "List of strings"
Utils.retrieveMsg[Bar](List(new Bar, new Bar)) // вернет "List of `Foo` inheritor instances"

Следует отметить, что, так как использование тегов типов подразумевает рефлексию, то это может значительно замедлить программу, поэтому использовать их нужно только там, где это действительно необходимо.

На момент написания статьи, версия Scala 2.12.1.

Полезные ссылки:
http://docs.scala-lang.org/overviews/reflection/typetags-manifests
https://medium.com/byte-code/overcoming-type-erasure-in-scala-8f2422070d20
https://scalerablog.wordpress.com/2015/12/21/classtag-class-and-war-stories/

Теги:
Добавить комментарий:
Комментариев: (0)
Опубликовать