Подтвердить что ты не робот

Scala контравариантность - пример реальной жизни

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

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

4b9b3361

Ответ 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]. Следовательно, контравариантность также может рассматриваться как "расширяющееся соотношение, поскольку типы" расширены от более специфических до более общих ".

Заключение

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

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