2011-12-26 17 views
26

Entiendo la covarianza y la contravarianza en scala. La covarianza tiene muchas aplicaciones en el mundo real, pero no puedo pensar en ninguna para aplicaciones de contravarianza, excepto en los mismos viejos ejemplos de Funciones.Contravalor de Scala: ejemplo de vida real

¿Alguien puede arrojar algo de luz sobre ejemplos del mundo real de contravariance usar?

+0

Vea la respuesta de Daniel Spiewak a http: // stackoverflow.com/questions/663254/scala-covariance-contravariance-question – sourcedelica

+3

... porque nadie usa funciones en el mundo real? =) –

Respuesta

20

En mi opinión, los dos ejemplos más simples después de Function están ordenando y la igualdad. Sin embargo, el primero no es contravariante en la biblioteca estándar de Scala, y el segundo ni siquiera existe en él. Entonces, voy a usar los equivalentes de Scalaz: Order y Equal.

A continuación, necesito una jerarquía de clases, preferiblemente una que sea familiar y, por supuesto, los dos conceptos anteriores deben tener sentido para ella. Si Scala tenía una superclase Number de todos los tipos numéricos, eso hubiera sido perfecto. Desafortunadamente, no tiene tal cosa.

Así que voy a intentar hacer los ejemplos con colecciones. Para hacerlo simple, consideremos Seq[Int] y List[Int]. Debe quedar claro que List[Int] es un subtipo de Seq[Int], es decir, List[Int] <: Seq[Int].

Entonces, ¿qué podemos hacer con él? En primer lugar, vamos a escribir algo que compara dos listas:

def smaller(a: List[Int], b: List[Int])(implicit ord: Order[List[Int]]) = 
    if (ord.order(a,b) == LT) a else b 

Ahora voy a escribir un implícito Order para 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 
} 

Con estas definiciones, ahora puedo hacer algo como esto:

scala> smaller(List(1), List(1, 2, 3)) 
res0: List[Int] = List(1) 

Nota que estoy pidiendo una Order[List[Int]], pero estoy pasando un Order[Seq[Int]]. Esto significa que Order[Seq[Int]] <: Order[List[Int]]. Dado que Seq[Int] >: List[Int], esto solo es posible debido a contra-varianza.

La siguiente pregunta es: ¿tiene algún sentido?

Consideremos smaller nuevamente. Quiero comparar dos listas de enteros. Naturalmente, cualquier cosa que compare dos listas es aceptable, pero ¿cuál es la lógica de algo que compara dos Seq[Int] siendo aceptable?

en la definición de seqOrder cómo se comparan los elementos parámetros a él. Obviamente, un List[Int] puede ser un parámetro para algo esperando un Seq[Int]. De esto se sigue que algo que algo que se compara Seq[Int] es aceptable en lugar de algo que se puede comparar con List[Int]: ambos se pueden usar con los mismos parámetros.

¿Qué pasa al revés? Supongamos que tengo un método que solo comparó :: (contras de la lista), que, junto con Nil, es un subtipo de List. Obviamente no pude usar esto, porque smaller bien podría recibir un Nil para comparar. Se deduce que no se puede usar Order[::[Int]] en lugar de Order[List[Int]].

Vamos a proceder a la igualdad, y escribir un método para ello:

def equalLists(a: List[Int], b: List[Int])(implicit eq: Equal[List[Int]]) = eq.equal(a, b) 

Debido Order extiende Equal, lo puedo usar con el mismo implícita arriba:

scala> equalLists(List(4, 5, 6), List(1, 2, 3)) // we are comparing lengths! 
res3: Boolean = true 

La lógica aquí es el el mismo. Cualquier cosa que pueda decir si dos Seq[Int] son lo mismo puede, obviamente, también decir si dos List[Int] son iguales. De eso, se deduce que Equal[Seq[Int]] <: Equal[List[Int]], que es verdadero porque Equal es contravariante.

+0

Hay scala.Equiv, pero tampoco es contravariante. – extempore

+0

En su ejemplo "más pequeño", podría haber hecho: 'def más pequeño (a: Lista [Int], b: Lista [Int]) (ord implícito: Orden [Seq [Int]]) = if (ord. orden (a, b) == LT) a else b'? en lugar de 'Ordenar [Lista [Int]]' y lograr el mismo propósito? ¿Por qué usar contravariancia? – Chao

+0

@Chao Sí, podría, pero luego no elegiría un pedido más específico. –

18

Este ejemplo es del último proyecto en el que estaba trabajando. Supongamos que tiene una clase de tipo PrettyPrinter[A] que proporciona lógica para objetos de impresión bonita del tipo A. Ahora bien, si B >: A (es decir, si B es la clase superior de A) y sabe cómo imprimir bastante B (es decir, tiene una instancia de PrettyPrinter[B] disponible), puede usar la misma lógica para imprimir bastante A. En otras palabras, B >: A implica PrettyPrinter[B] <: PrettyPrinter[A]. Para que pueda declarar PrettyPrinter[A] contravariante en 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)] 

Algunos otros ejemplos habría Ordering tipo de clase de biblioteca estándar Scala, Equal, Show (isomorfo a PrettyPrinter arriba), Resource tipo-clases de Scalaz etc.

Editar:
Como señaló Daniel, el Ordering de Scala no es contravariante. (Realmente no sé por qué). En su lugar, puede considerar scalaz.Order, que tiene el mismo propósito que scala.Ordering pero es contravariante en su parámetro de tipo.

Adición:
relación Supertype-subtipo es más que un tipo de relación que puede existir entre dos tipos. Puede haber muchas de esas relaciones posibles. Consideremos dos tipos A y B relacionados con la función f: B => A (es decir, una relación arbitraria). Se dice que el tipo de datos F[_] es un functor contravariante si se puede definir una operación contramap para que pueda levantar una función del tipo B => A a F[A => B].

Las siguientes leyes deben ser satisfechos:

  1. x.contramap(identity) == x
  2. x.contramap(f).contramap(g) == x.contramap(f compose g)

Todos los tipos de datos discutidos anteriormente (Show, Equal etc.) son funtores contravariantes. Esta propiedad nos permite hacer cosas útiles como el que se ilustra a continuación:

Suponga que tiene una clase Candidate definido como:

case class Candidate(name: String, age: Int) 

Es necesario un Order[Candidate] que ordena los candidatos por su edad. Ahora sabe que hay una instancia Order[Int] disponible.Se puede obtener una instancia Order[Candidate] de que con la operación contramap:

val byAgeOrder: Order[Candidate] = 
    implicitly[Order[Int]] contramap ((_: Candidate).age) 
2

Un ejemplo basado en un sistema de software basado en eventos del mundo real. Tal sistema se basa en categorías amplias de eventos, como eventos relacionados con el funcionamiento del sistema (eventos del sistema), eventos generados por acciones del usuario (eventos del usuario), etc.

Una posible jerarquía de eventos:

trait Event 

trait UserEvent extends Event 

trait SystemEvent extends Event 

trait ApplicationEvent extends SystemEvent 

trait ErrorEvent extends ApplicationEvent 

Ahora los programadores que trabajan en el sistema de eventos impulsado por la necesidad de encontrar una manera de registrar/procesar los eventos generados en el sistema. Creará un rasgo, Sink, que se usa para marcar los componentes que necesitan ser notificados cuando se dispara un evento.

trait Sink[-In] { 
    def notify(o: In) 
} 

Como consecuencia de marcar el parámetro de tipo con el símbolo -, el tipo Sink convirtió contravariant.

Una posible forma de notificar a las partes interesadas que sucedió un evento es escribir un método y pasarlo al evento correspondiente. Este método hipotéticamente hacer algo de procesamiento y luego se encargará de notificar al receptor de sucesos:

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

Un par de implementaciones hipotéticos fregadero.

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 = ??? 
} 

Las siguientes llamadas a métodos son aceptados por el compilador:

appEventFired(new ApplicationEvent {}, ses) 

errorEventFired(new ErrorEvent {}, ges) 

appEventFired(new ApplicationEvent {}, ges) 

En cuanto a la serie de llamadas se nota que es posible llamar a un método esperando una Sink[ApplicationEvent] con un Sink[SystemEvent] e incluso con una Sink[Event]. Además, puede llamar al método esperando un Sink[ErrorEvent] con un Sink[Event].

Al reemplazar la invarianza con una restricción de contravarianza, un Sink[SystemEvent] se convierte en un subtipo de Sink[ApplicationEvent]. Por lo tanto, la contravarianza también se puede considerar como una relación de "ampliación", ya que los tipos se "amplían" de más específicos a más genéricos.

Conclusión

Este ejemplo ha sido descrito en una serie de artículos sobre la variación que se encuentran en my blog

Al final, creo que también ayuda a entender la teoría detrás de él ...

+0

la mejor respuesta aquí –

Cuestiones relacionadas