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