Нам нужен оператор типа Or[U,V]
, который можно использовать для ограничения параметров типа X
таким образом, чтобы либо X <: U
, либо X <: V
.Вот определение, которое подходит как можно ближе:
trait Inv[-X]
type Or[U,T] = {
type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}
Вот как оно используется:
// use
class A; class B extends A; class C extends B
def foo[X : (B Or String)#pf] = {}
foo[B] // OK
foo[C] // OK
foo[String] // OK
foo[A] // ERROR!
foo[Number] // ERROR!
Здесь используются несколько трюков типа Scala.Основным является использование обобщенных типовых ограничений .Для типов U
и V
компилятор Scala предоставляет класс с именем U <:< V
(и неявный объект этого класса) тогда и только тогда, когда компилятор Scala может доказать, что U
является подтипом V
.Вот более простой пример использования ограничений обобщенного типа, который работает в некоторых случаях:
def foo[X](implicit ev : (B with String) <:< X) = {}
Этот пример работает, когда X
экземпляр класса B
, String
или имеет тип, который не является нисупертип, ни подтип B
или String
.В первых двух случаях это верно по определению ключевого слова with
, которое (B with String) <: B
и (B with String) <: String
, поэтому Scala предоставит неявный объект, который будет передан как ev
: компилятор Scala правильно примет foo[B]
и foo[String]
.
В последнем случае я полагаюсь на то, что если U with V <: X
, то U <: X
или V <: X
.Это кажется интуитивно верным, и я просто предполагаю это.Из этого предположения ясно, почему этот простой пример не срабатывает, когда X
является супертипом или подтипом либо B
, либо String
: например, в приведенном выше примере неверно принимается foo[A]
, а отклоняется foo[C]
,Опять же, нам нужно выражение типа для переменных U
, V
и X
, которое истинно именно тогда, когда X <: U
или X <: V
.
понятие контравариантности Scala можетпомогите здесь.Помните черту trait Inv[-X]
?Потому что он контравариантен в своем типе параметра X
, Inv[X] <: Inv[Y]
тогда и только тогда, когда Y <: X
.Это означает, что мы можем заменить приведенный выше пример на тот, который на самом деле будет работать:
trait Inv[-X]
def foo[X](implicit ev : (Inv[B] with Inv[String]) <:< Inv[X]) = {}
Это потому, что выражение (Inv[U] with Inv[V]) <: Inv[X]
истинно, в соответствии с тем же предположением выше, именно тогда, когда Inv[U] <: Inv[X]
или Inv[V] <: Inv[X]
и по определению контравариантности это верно именно тогда, когда X <: U
или X <: V
.
Можно сделать вещи немного более многократно используемыми, объявив параметризуемый тип BOrString[X]
и используя его следующим образом:
trait Inv[-X]
type BOrString[X] = (Inv[B] with Inv[String]) <:< Inv[X]
def foo[X](implicit ev : BOrString[X]) = {}
Теперь Scala будет пытаться создать тип BOrString[X]
для каждого X
, с которым вызывается foo
, и этот тип будет создан именно тогда, когда X
является подтипом либо B
или String
.Это работает, и есть сокращенная запись.Синтаксис ниже эквивалентен (за исключением того, что ev
теперь должен указываться в теле метода как implicitly[BOrString[X]]
, а не просто ev
) и использует BOrString
в качестве границы контекста типа :
def foo[X : BOrString] = {}
Что нам действительно нужно, так это гибкий способ создания привязки к контексту типа.Контекст типа должен быть параметризуемым типом, и мы хотим, чтобы его можно было параметризировать.Звучит так, будто мы пытаемся каррировать функции на типах так же, как мы каррируем функции на значениях.Другими словами, мы хотели бы что-то вроде следующего:
type Or[U,T][X] = (Inv[U] with Inv[T]) <:< Inv[X]
Это не возможно напрямую в Scala, но есть способ, который мы можем использовать, чтобы быть довольно близко.Это приводит нас к определению Or
выше:
trait Inv[-X]
type Or[U,T] = {
type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}
Здесь мы используем структурную типизацию и оператор фунта Scala для создания структурного типа Or[U,T]
это гарантированно имеет один внутренний тип.Это странный зверь.Чтобы получить некоторый контекст, функция def bar[X <: { type Y = Int }](x : X) = {}
должна вызываться с подклассами AnyRef
, которые имеют определенный тип Y
:
bar(new AnyRef{ type Y = Int }) // works!
Использование оператора фунта позволяет нам обращаться к внутреннемувведите Or[B, String]#pf
и, используя инфиксную запись для оператора типа Or
, мы приходим к нашему первоначальному определению foo
:
def foo[X : (B Or String)#pf] = {}
Мы можем использовать тот факт, что функциятипы являются контравариантными в параметре первого типа, чтобы избежать определения черты Inv
:
type Or[U,T] = {
type pf[X] = ((U => _) with (T => _)) <:< (X => _)
}