Scala контравариантность - пример реальной жизни
Я понимаю ковариацию и контравариантность в scala. Ковариация имеет множество приложений в реальном мире, но я не могу придумать никаких приложений для контравариантности, кроме тех же старых примеров для функций.
Может ли кто-то пролить свет на примеры реального мира из contravariance
использовать?
Ответы
Ответ 1
По моему мнению, двумя самыми простыми примерами после Function
являются упорядочение и равенство. Однако первая не противоречит варианту в стандартной библиотеке Scala, а вторая даже не существует в ней. Итак, я собираюсь использовать эквиваленты Scalaz: Order и Equal.
Далее мне нужна иерархия классов, желательно знакомая, и, конечно же, обе концепции должны иметь смысл для нее. Если бы Scala обладал суперклассом Number
всех числовых типов, это было бы идеально. К сожалению, у этого нет такой вещи.
Итак, я попытаюсь сделать примеры с коллекциями. Чтобы сделать его простым, давайте просто рассмотрим Seq[Int]
и List[Int]
. Ясно, что List[Int]
является подтипом Seq[Int]
, т.е. List[Int] <: Seq[Int]
.
Итак, что мы можем с этим сделать? Во-первых, напишите что-нибудь, сравнивающее два списка:
def smaller(a: List[Int], b: List[Int])(implicit ord: Order[List[Int]]) =
if (ord.order(a,b) == LT) a else b
Теперь я напишу неявный Order
для Seq[Int]
:
implicit val seqOrder = new Order[Seq[Int]] {
def order(a: Seq[Int], b: Seq[Int]) =
if (a.size < b.size) LT
else if (b.size < a.size) GT
else EQ
}
С помощью этих определений теперь я могу сделать что-то вроде этого:
scala> smaller(List(1), List(1, 2, 3))
res0: List[Int] = List(1)
Обратите внимание, что я прошу Order[List[Int]]
, но я передаю Order[Seq[Int]]
. Это означает, что Order[Seq[Int]] <: Order[List[Int]]
. Учитывая, что Seq[Int] >: List[Int]
, это возможно только из-за противоречия.
Следующий вопрос: имеет ли смысл?
Рассмотрим снова smaller
. Я хочу сравнить два списка целых чисел. Естественно, что все, что сравнивает два списка, приемлемо, но какая логика того, что сравнивает два Seq[Int]
, приемлемо?
Обратите внимание на определение seqOrder
того, как сравниваемые вещи становятся для него параметрами. Очевидно, a List[Int]
может быть параметром для чего-то ожидающего a Seq[Int]
. Из этого следует, что что-то, что сравнивает Seq[Int]
, приемлемо вместо того, что сравнивает List[Int]
: оба они могут использоваться с теми же параметрами.
Как насчет обратного? Скажем, у меня был метод, который сравнивал только ::
(список cons), который вместе с Nil
является подтипом List
. Я, очевидно, не мог использовать это, потому что smaller
вполне мог бы сравнить Nil
. Из этого следует, что Order[::[Int]]
нельзя использовать вместо Order[List[Int]]
.
Перейдем к равенству и напишем для него метод:
def equalLists(a: List[Int], b: List[Int])(implicit eq: Equal[List[Int]]) = eq.equal(a, b)
Поскольку Order
extends Equal
, я могу использовать его с тем же неявным выше:
scala> equalLists(List(4, 5, 6), List(1, 2, 3)) // we are comparing lengths!
res3: Boolean = true
Логика здесь одна и та же. Все, что может сказать, являются ли два Seq[Int]
одинаковыми, может, очевидно, также сказать, являются ли два List[Int]
одинаковыми. Из этого следует, что Equal[Seq[Int]] <: Equal[List[Int]]
, что верно, потому что Equal
является контравариантным.
Ответ 2
Этот пример из последнего проекта, над которым я работал. Скажем, у вас есть тип-класс PrettyPrinter[A]
, который обеспечивает логику для довольно-печатающих объектов типа A
. Теперь, если B >: A
(т.е. Если B
является суперклассом A
), и вы знаете, как довольно-печатать B
(т.е. Иметь экземпляр PrettyPrinter[B]
), тогда вы можете использовать ту же логику, напечатать A
. Другими словами, B >: A
влечет PrettyPrinter[B] <: PrettyPrinter[A]
. Таким образом, вы можете объявить PrettyPrinter[A]
контравариантным на A
.
scala> trait Animal
defined trait Animal
scala> case class Dog(name: String) extends Animal
defined class Dog
scala> trait PrettyPrinter[-A] {
| def pprint(a: A): String
| }
defined trait PrettyPrinter
scala> def pprint[A](a: A)(implicit p: PrettyPrinter[A]) = p.pprint(a)
pprint: [A](a: A)(implicit p: PrettyPrinter[A])String
scala> implicit object AnimalPrettyPrinter extends PrettyPrinter[Animal] {
| def pprint(a: Animal) = "[Animal : %s]" format (a)
| }
defined module AnimalPrettyPrinter
scala> pprint(Dog("Tom"))
res159: String = [Animal : Dog(Tom)]
Некоторые другие примеры: <класs > Ordering
тип-класс из стандартной библиотеки Scala, Equal
, Show
(изоморфный выше PrettyPrinter
), Resource
классы типов от Scalaса и т.д.
Edit:
Как указывал Даниил, Scala Ordering
не является контравариантным. (Я действительно не знаю почему.) Вместо этого вы можете рассмотреть scalaz.Order
, который предназначен для той же цели, что и scala.Ordering
, но контравариантен по его параметру типа.
Добавление:
Отношение супертипа-подтипа - это только один тип отношений, который может существовать между двумя типами. Могут быть много таких отношений. Рассмотрим два типа A
и B
, связанные с функцией f: B => A
(т.е. Произвольное отношение). Тип данных F[_]
называется контравариантным функтором, если вы можете определить для него операцию contramap
, которая может поднять функцию типа B => A
до F[A => B]
.
Необходимо выполнить следующие законы:
-
x.contramap(identity)
== x
-
x.contramap(f).contramap(g)
== x.contramap(f compose g)
Все описанные выше типы данных (Show
, Equal
и т.д.) являются контравариантными функторами. Это свойство позволяет нам делать полезные вещи, например, приведенные ниже:
Предположим, что у вас есть класс Candidate
, определенный как:
case class Candidate(name: String, age: Int)
Вам нужен Order[Candidate]
, который заказывает кандидатов по их возрасту. Теперь вы знаете, что существует экземпляр Order[Int]
. Вы можете получить экземпляр Order[Candidate]
с помощью операции contramap
:
val byAgeOrder: Order[Candidate] =
implicitly[Order[Int]] contramap ((_: Candidate).age)
Ответ 3
Пример, основанный на реальной программной системе, управляемой событиями. Такая система основана на широких категориях событий, таких как события, связанные с функционированием системы (системные события), события, генерируемые действиями пользователя (пользовательские события) и т.д.
Возможная иерархия событий:
trait Event
trait UserEvent extends Event
trait SystemEvent extends Event
trait ApplicationEvent extends SystemEvent
trait ErrorEvent extends ApplicationEvent
Теперь программисты, работающие над системой, управляемой событиями, должны найти способ регистрации/обработки событий, генерируемых в системе. Они создадут признак Sink
, который используется для отметки компонентов, которые должны быть уведомлены, когда событие было запущено.
trait Sink[-In] {
def notify(o: In)
}
В результате маркировки параметра типа с символом -
тип Sink стал контравариантным.
Возможный способ уведомить заинтересованные стороны о том, что произошло событие - написать метод и передать ему соответствующее событие. Этот метод будет гипотетически выполнять некоторую обработку, а затем он будет следить за уведомлением о приемнике событий:
def appEventFired(e: ApplicationEvent, s: Sink[ApplicationEvent]): Unit = {
// do some processing related to the event
// notify the event sink
s.notify(e)
}
def errorEventFired(e: ErrorEvent, s: Sink[ErrorEvent]): Unit = {
// do some processing related to the event
// notify the event sink
s.notify(e)
}
Несколько гипотетических реализаций стока.
trait SystemEventSink extends Sink[SystemEvent]
val ses = new SystemEventSink {
override def notify(o: SystemEvent): Unit = ???
}
trait GenericEventSink extends Sink[Event]
val ges = new GenericEventSink {
override def notify(o: Event): Unit = ???
}
Компилятор принимает следующие вызовы методов:
appEventFired(new ApplicationEvent {}, ses)
errorEventFired(new ErrorEvent {}, ges)
appEventFired(new ApplicationEvent {}, ges)
Рассматривая серию вызовов, вы замечаете, что можно вызвать метод, ожидающий Sink[ApplicationEvent]
с Sink[SystemEvent]
и даже с Sink[Event]
. Кроме того, вы можете вызвать метод, ожидающий Sink[ErrorEvent]
с Sink[Event]
.
Заменяя инвариантность на ограничение контравариантности, a Sink[SystemEvent]
становится подтипом Sink[ApplicationEvent]
. Следовательно, контравариантность также может рассматриваться как "расширяющееся соотношение, поскольку типы" расширены от более специфических до более общих ".
Заключение
Этот пример был описан в серии статей о дисперсии, найденной в в моем блоге
В конце концов, я думаю, что это помогает также понять теорию, лежащую в ее основе...