Скала контравариантность - пример из реальной жизни - PullRequest
27 голосов
/ 26 декабря 2011

Я понимаю ковариацию и контравариантность в скале. Covariance имеет много приложений в реальном мире, но я не могу придумать ни одного для контравариантных приложений, кроме тех же самых старых примеров для функций.

Может ли кто-нибудь пролить свет на примеры из реального мира из contravariance использования?

Ответы [ 3 ]

20 голосов
/ 26 декабря 2011

На мой взгляд, два самых простых примера после 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, как сравниваемые вещи становятся параметрами к нему.Очевидно, что List[Int] может быть параметром для чего-то ожидающего Seq[Int].Из этого следует, что что-то, что сравнивает Seq[Int], приемлемо вместо чего-то, что сравнивает List[Int]: они оба могут использоваться с одинаковыми параметрами.

Как насчет обратного?Допустим, у меня был метод, который сравнивал только :: (список минусов), который вместе с 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 расширяет 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 противоречиво.

18 голосов
/ 26 декабря 2011

Этот пример взят из последнего проекта, над которым я работал. Скажем, у вас есть класс типов 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)]

Некоторые другие примеры: Ordering класс типов из стандартной библиотеки Scala, Equal, Show (изоморфный PrettyPrinter выше), Resource классы типов из Scalaz и т. .

Edit:
Как указал Даниэль, Ordering Скалы не противоречиво. (Я действительно не знаю, почему.) Вместо этого вы можете рассмотреть scalaz.Order, который предназначен для той же цели, что и scala.Ordering, но противоречив по своему параметру типа.

Добавление:
Отношение супертип-подтип - это всего лишь один тип отношений, которые могут существовать между двумя типами. Таких отношений может быть много. Рассмотрим два типа A и B, связанные с функцией f: B => A (то есть произвольное отношение). Тип данных F[_] называется контравариантным функтором, если вы можете определить для него операцию contramap, которая может поднимать функцию типа B => A до F[A => B].

Необходимо соблюдать следующие законы:

  1. x.contramap(identity) == x
  2. 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)
4 голосов
/ 21 декабря 2015

Пример, основанный на реальной управляемой событиями программной системе. Такая система основана на широких категориях событий, таких как события, связанные с функционированием системы (системные события), события, генерируемые действиями пользователя (пользовательские события) и т. Д.

Возможная иерархия событий:

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)
}

Пара гипотетических реализаций Sink.

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].

Заменив инвариантность на ограничение контравариантности, Sink[SystemEvent] становится подтипом Sink[ApplicationEvent]. Следовательно, контравариантность также можно рассматривать как «расширяющиеся» отношения, поскольку типы «расширены» от более специфических к более общим.

Заключение

Этот пример был описан в серии статей о дисперсии, найденных в моем блоге

В конце концов, я думаю, что это также помогает понять теорию, стоящую за этим ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...