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

Я поймал себя на том, что смотрю отрывок из записи Scalawags#2, а затем идет эта часть о стирании шрифта и Дике Уолле, указывающем, что отражение в конце концов укусит вас за ноги.

Итак, я подумал о чем-то, что я делаю довольно часто (и я также видел реализацию коллекций Scala). Допустим, у меня есть система с сериализатором, принимающим систему в качестве параметра типа:

trait Sys[S <: Sys[S]] { type Tx }
trait FooSys extends Sys[FooSys]

trait Serializer[S <: Sys[S], A] {
  def read(implicit tx: S#Tx): A
}

Теперь существует много типов A, для которых сериализаторы могут быть созданы без параметров-значений, поэтому, по сути, параметр системного типа является "пустым". И поскольку сериализаторы в моем примере активно вызываются, я сохраняю инстанцирование:

object Test {
  def serializer[S <: Sys[S]] : Serializer[S, Test[S]] =
    anySer.asInstanceOf[Ser[S]]

  private val anySer = new Ser[FooSys]

  private final class Ser[S <: Sys[S]] extends Serializer[S, Test[S]] {
    def read(implicit tx: S#Tx) = new Test[S] {} // (shortened for the example)
  }
}
trait Test[S <: Sys[S]]

Я знаю, что это правильно, но, конечно, asInstanceOf имеет неприятный запах. Есть ли какие-либо предложения по этому подходу? Позвольте мне добавить две вещи.

  • перемещение параметра типа из конструктора типажа Serializer в метод read не является вариантом (существуют специальные сериализаторы, для которых требуются аргументы-значения, параметризованные в S)
  • добавление дисперсии к параметру конструктора типа Serializer не вариант

person 0__    schedule 18.01.2013    source источник


Ответы (1)


Введение:

Меня немного смущает ваш пример, и я, возможно, неправильно понял ваш вопрос, у меня такое чувство, что между S и Tx существует определенная рекурсия типов, которую я не получаю из вашего вопроса (потому что если нет, S#Tx может быть чем угодно, и я не не понимаю проблемы с anySer)

Предварительный ответ:

Во время компиляции для любого экземпляра Ser[T] будет четко определенный параметр типа T, поскольку вы хотите сохранить его при создании экземпляра, у вас будет один anySer Ser[T] для данного конкретного типа A

Вы каким-то образом говорите, что Ser[A] будет работать как Ser[S] для любого S. Это можно объяснить двояко, исходя из соотношения между типами А и S.

  1. Если это преобразование возможно для каждого A<:<S, тогда ваш сериализатор — COVARIANT, и вы можете инициализировать свой anySer как Ser[Nothing]. Поскольку Nothing является подклассом каждого класса в Scala, ваш anySer всегда будет работать как Ser[Whatever].

  2. Если это преобразование возможно для каждого S<:<A, тогда ваш сериализатор — CONTRAVARIANT, и вы можете инициализировать свой anySer как Ser[Any]. Поскольку Any является подклассом каждого класса в Scala, ваш anySer всегда будет работать как Ser[Whatever].

  3. Если это не тот из предыдущего случая, то это означает, что:

    def serializer[S <: Sys[S]] : Serializer[S, Test[S]] =
    anySer.asInstanceOf[Ser[S]]
    

    Может привести к ужасному сбою во время выполнения, потому что будут некоторые S, для которых сериализатор не будет работать. Если нет таких S, для которых это могло бы произойти, то ваш класс попадает либо в 1, либо в

Комментарий после редактирования

Если ваши типы действительно инвариантны, преобразование через приведение нарушает отношение инвариантности. По сути, вы заставляете систему типов выполнять неестественное преобразование, потому что знаете, что ничего плохого не произойдет, основываясь на ваших собственных знаниях кода, который вы написали. Если это так, то кастинг — правильный путь: вы форсируете тип, отличный от того, который компилятор может проверить формально, и вы делаете это явным. Я бы даже написал большой комментарий о том, почему вы знаете, что операция является законной, а компилятор не может догадаться, и в конечном итоге прикрепил бы красивый модульный тест, чтобы убедиться, что «неформальное» отношение всегда выполняется.

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

person Edmondo1984    schedule 18.01.2013
comment
В реальной реализации S#Tx, конечно, будет иметь полезную верхнюю границу. В противном случае ваша интерпретация верна, Ser[A] будет работать как Ser[S] для любого S, с добавлением, что очевидно S <: Sys[S]]. Итак, случай № 2 имеет место. Но поскольку S инвариантно в Sys, оно также должно быть инвариантным в Serializer, поэтому я не могу вернуть Ser[Any]. - person 0__; 18.01.2013