Использование Scala Неявно для Типового равенства

Я читал некоторые вещи о программировании уровня Scala. В основном блог Apocalisp, а также выступление Александра Лемана на youtube.

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

implicitly[Int =:= Int]

В блоге Apocalisp отмечается:

Это полезно для захвата неявного значения, которое находится в области видимости и имеет тип T.

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

В вышеприведенном случае существует неявный тип "Int" в области видимости, который "неявно" вырывается из эфира, позволяя компилировать код? Как это соотносится с возвращаемым типом функции function1?

res0: =:=[Int,Int] = <function1>

Кроме того, откуда это подразумевается? Как насчет в случае с моей чертой "Foo", почему

implicitly[Foo =:= Foo] 

компилировать? Где в этом случае подразумевается "Foo"?

Извините заранее, если это очень глупый вопрос и спасибо за любую помощь!

Ответ 1

X =:= Y - это просто синтаксический сахар (инфиксная запись) для типа =:=[X, Y].

Поэтому, когда вы делаете implicitly[Y =:= Y], вы просто ищете неявное значение типа =:=[X, Y]. =:= - это общая черта, определенная в Predef.

Кроме того, =:= является допустимым именем типа, поскольку имена типов (как и любой идентификатор) могут содержать специальные символы.

Теперь переименуйте =:= в IsSameType и удалите запись инфикса, чтобы сделать наш код более разборчивым и понятным. Это дает нам implicitly[IsSameType[X,Y]]

Вот упрощенная версия определения этого типа:

sealed abstract class IsSameType[X, Y]
object IsSameType {
   implicit def tpEquals[A] = new IsSameType[A, A]{}
}

Обратите внимание, как tpEquals предоставляет неявное значение IsSameType[A, A] для любого типа A. Другими словами, он предоставляет неявное значение IsSameType[X, Y] тогда и только тогда, когда X и Y имеют одинаковый тип. Так что implicitly[IsSameType[Foo, Foo]] компилируется нормально. Но implicitly[IsSameType[Int, String]] этого не делает, поскольку в области видимости типа IsSameType[Int, String] нет неявного, учитывая, что tpEquals здесь неприменим.

Таким образом, с помощью этой очень простой конструкции мы можем статически проверить, что некоторый тип X совпадает с другим типом Y.


Теперь вот пример того, как это может быть полезно. Скажем, я хочу определить тип Pair (игнорируя тот факт, что он уже существует в стандартной библиотеке):

case class Pair[X,Y]( x: X, y: Y ) {
  def swap: Pair[Y,X] = Pair( y, x )
}

Pair параметризован с типами его 2 элементов, которые могут быть чем угодно, и что наиболее важно, не связаны. А что если я захочу определить метод toList, который преобразует пару в список из 2 элементов? Этот метод действительно имеет смысл только в том случае, когда X и Y одинаковы, в противном случае я был бы вынужден вернуть List[Any]. И я, конечно, не хочу менять определение Pair на Pair[T]( x: T, y: T ), потому что я действительно хочу иметь возможность иметь пары разнородных типов. В конце концов, только при вызове toList мне нужно, чтобы X == Y. Все другие методы (такие как swap) должны вызываться на любой гетерогенной паре. Итак, в конце я действительно хочу статически убедиться, что X == Y, но только при вызове toList, и в этом случае становится возможным и последовательным возвращать List[X] (или List[Y], что является тем же самым ):

case class Pair[X,Y]( x: X, y: Y ) {
  def swap: Pair[Y,X] = Pair( y, x )
  def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = ???
}

Но все еще существует серьезная проблема, когда дело доходит до реализации toList. Если я попытаюсь написать очевидную реализацию, это не скомпилируется:

def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = List[Y]( x, y )

Компилятор будет жаловаться, что x не относится к типу Y. И действительно, X и Y все еще являются разными типами в том, что касается компилятора. Только благодаря тщательной конструкции мы можем быть статически уверены, что X == Y (а именно тот факт, что toList принимает неявное значение типа IsSameType[X, Y], и что они предоставляются методом tpEquals, только если X == Y). Но компилятор определенно не расшифрует эту хитрую конструкцию, чтобы сделать вывод, что X == Y.

Чтобы исправить эту ситуацию, мы можем обеспечить неявное преобразование из X в Y при условии, что мы знаем, что X == Y (или, другими словами, у нас есть экземпляр IsSameType[X, Y] в области видимости).

// A simple cast will do, given that we statically know that X == Y
implicit def sameTypeConvert[X,Y]( x: X )( implicit evidence: IsSameType[X, Y] ): Y = x.asInstanceOf[Y]

И теперь наша реализация toList наконец-то скомпилируется нормально: x будет просто преобразована в Y посредством неявного преобразования sameTypeConvert.

В качестве окончательной настройки мы можем упростить ситуацию еще больше: учитывая, что мы уже принимаем неявное значение (evidence) в качестве параметра, почему бы ЭТОМУ ЦЕННОСТИ не осуществить преобразование? Вот так:

sealed abstract class IsSameType[X, Y] extends (X => Y) {
  def apply( x: X ): Y = x.asInstanceOf[Y]
}
object IsSameType {
   implicit def tpEquals[A] = new IsSameType[A, A]{}
}    

Затем мы можем удалить метод sameTypeConvert, поскольку неявное преобразование теперь обеспечивается самим экземпляром IsSameType. Теперь IsSameType выполняет двойную цель: статически гарантирует, что X == Y, и (если это так) предоставляет неявное преобразование, которое фактически позволяет нам рассматривать экземпляры X как экземпляры Y.

Теперь мы в основном переопределили тип =:=, как определено в Predef


ОБНОВЛЕНИЕ: Из комментариев кажется очевидным, что использование asInstanceOf беспокоит людей (хотя это действительно просто деталь реализации, и ни один пользователь IsSameType не должен когда-либо выполнять приведение). Оказывается, от него легко избавиться даже в реализации. Вот:

sealed abstract class IsSameType[X, Y] extends (X => Y) {
  def apply(x: X): Y
}
object IsSameType {
  implicit def tpEquals[A] = new IsSameType[A, A]{
    def apply(x: A): A = x
  }
}

По сути, мы просто оставляем аннотацию apply и реализуем ее только в tpEquals, где мы (и компилятор) знаем, что и переданный аргумент, и возвращаемое значение действительно имеют одинаковый тип. Следовательно, нет необходимости в любом актерском составе. Это действительно так.

Обратите внимание, что, в конце концов, то же преобразование все еще присутствует в сгенерированном байт-коде, но теперь отсутствует в исходном коде и корректно корректно с точки зрения компилятора. И хотя мы ввели дополнительный (анонимный) класс (и, следовательно, дополнительную косвенность от абстрактного класса к конкретному классу), он должен работать так же быстро на любой приличной виртуальной машине, потому что мы находимся в простом случай "отправки мономорфного метода" (посмотрите, если вы заинтересованы во внутренней работе виртуальных машин). Хотя виртуальной машине все еще может быть труднее встроить вызов в apply (оптимизация виртуальной машины во время выполнения является чем-то черным, и сложно сделать определенные заявления).

И последнее замечание: я должен подчеркнуть, что на самом деле не так уж важно иметь приведение в коде, если оно корректно доказуемо. И в конце концов, в самой стандартной библиотеке до недавнего времени использовалась та же самая версия (теперь вся реализация была переработана, чтобы сделать ее более мощной, но все же содержит преобразования в других местах). Если это достаточно хорошо для стандартной библиотеки, это достаточно хорошо для меня.