2011-12-26 11 views
26

Comprendo la covarianza e la contravarianza in scala. La Covariance ha molte applicazioni nel mondo reale, ma non posso pensare a nessuna delle applicazioni di controvarianza, eccetto gli stessi vecchi esempi di Funzioni.Contravarianza scala - esempio di vita reale

Qualcuno può fare luce sugli esempi di mondo reale di contravariance?

+0

Vedere la risposta di Daniel Spiewak a http: // stackoverflow.it/questions/663254/scala-covariance-contravariance-question – sourcedelica

+3

... perché nessuno usa funzioni nel mondo reale? =) –

risposta

20

A mio parere, i due più semplici esempi dopo Function sono ordinare e l'uguaglianza. Tuttavia, il primo non è contro-variante nella libreria standard di Scala, e il secondo non esiste nemmeno in esso. Quindi, userò gli equivalenti Scalaz: Order e Equal.

Successivamente, ho bisogno di una gerarchia di classi, preferibilmente una che sia familiare e, ovviamente, entrambi i concetti di cui sopra devono avere senso per questo. Se Scala avesse una superclasse Number di tutti i tipi numerici, sarebbe stato perfetto. Sfortunatamente, non ha niente del genere.

Così ho intenzione di provare a rendere gli esempi con le collezioni. Per semplificare, consideriamo solo Seq[Int] e List[Int]. Dovrebbe essere chiaro che List[Int] è un sottotipo di Seq[Int], ovvero List[Int] <: Seq[Int].

Quindi, cosa possiamo fare con esso? In primo luogo, cerchiamo di scrivere qualcosa che confronta due liste:

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

Ora ho intenzione di scrivere un implicito Order per 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 queste definizioni, ora posso fare qualcosa di simile:

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

Nota che sto chiedendo una Order[List[Int]], ma sto passando un Order[Seq[Int]]. Ciò significa che Order[Seq[Int]] <: Order[List[Int]]. Dato che Seq[Int] >: List[Int], questo è possibile solo a causa della contro-varianza.

La prossima domanda è: ha senso?

Consideriamo nuovamente smaller. Voglio confrontare due liste di numeri interi. Naturalmente, tutto ciò che confronta due elenchi è accettabile, ma qual è la logica di qualcosa che mette a confronto due Seq[Int] accettabili?

Nota nella definizione di seqOrder in che modo gli oggetti confrontati diventano parametri. Ovviamente, un List[Int] può essere un parametro per qualcosa che si aspetta un Seq[Int]. Ne consegue che qualcosa che confronta Seq[Int] è accettabile al posto di qualcosa che confronta List[Int]: entrambi possono essere utilizzati con gli stessi parametri.

E il contrario? Diciamo che ho avuto un metodo che ha solo confrontato :: (elenco di svantaggi), che, insieme a Nil, è un sottotipo di List. Ovviamente non potrei usare questo, perché smaller potrebbe ricevere uno Nil da confrontare. Ne consegue che non è possibile utilizzare Order[::[Int]] anziché Order[List[Int]].

Procediamo all'uguaglianza, e scrivere un metodo per esso:

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

Poiché Order estende Equal, posso usare con lo stesso implicito sopra:

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

La logica è il lo stesso. Tutto ciò che può dire se due Seq[Int] sono uguali può, ovviamente, anche dire se due List[Int] sono uguali. Da ciò segue che Equal[Seq[Int]] <: Equal[List[Int]], che è vero perché Equal è contro-variante.

+0

C'è scala.Equiv, ma non è nemmeno controverso. – extempore

+0

Nel tuo esempio "più piccolo", avrei potuto fare: 'def più piccolo (a: List [Int], b: List [Int]) (implicito ord: Order [Seq [Int]]) = if (ord. ordine (a, b) == LT) a else b'? invece di "Ordinare [Elenco [Int]]" e ottenere lo stesso scopo? Perché usare la contravarianza? – Chao

+0

@Chao Sì, è possibile, ma in questo caso non si otterrebbe un ordine più specifico. –

18

Questo esempio proviene dall'ultimo progetto su cui stavo lavorando. Supponiamo che tu disponga di una classe di tipo PrettyPrinter[A] che fornisce la logica per oggetti di stampa graziosa di tipo A. Ora se B >: A (ad esempio se B è la superclasse di A) e sai come stampare graziosamente B (ad esempio, è disponibile un'istanza di PrettyPrinter[B]), allora puoi usare la stessa logica per stampare piuttosto A. In altre parole, B >: A implica PrettyPrinter[B] <: PrettyPrinter[A]. Quindi è possibile dichiarare controvariante PrettyPrinter[A] su 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)] 

Alcuni altri esempi sarebbero Ordering tipo di classe da libreria standard Scala, Equal, Show (isomorfo a PrettyPrinter sopra), Resource tipo-classi da Scalaz ecc

Edit:
Come ha sottolineato Daniel, lo Ordering di Scala non è controverso. (Davvero non so perché.) Si può invece considerare scalaz.Order che è inteso per lo stesso scopo di ma è controverso sul suo parametro tipo.

addendum:
rapporto Supertype sottotipo è però un tipo di rapporto che può esistere tra due tipi. Ci possono essere molte relazioni di questo tipo possibili. Consideriamo due tipi A e B relativi alla funzione f: B => A (cioè una relazione arbitraria). Si dice che il tipo di dati F[_] sia un functor controvariante se è possibile definire un'operazione contramap per esso che può sollevare una funzione di tipo B => A a F[A => B].

Le seguenti leggi devono essere soddisfatti:

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

Tutti i tipi di dati di cui sopra (Show, Equal etc.) sono funtori controvarianti. Questa struttura ci permette di fare cose utili come quello illustrato di seguito:

Supponiamo di avere una classe Candidate definito come:

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

Avete bisogno di un Order[Candidate] che ordina i candidati per la loro età. Ora sai che esiste un'istanza Order[Int] disponibile.È possibile ottenere un'istanza Order[Candidate] da quella con l'operazione contramap:

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

Un esempio basato su un sistema software basato sugli eventi reali. Tale sistema si basa su ampie categorie di eventi, come eventi relativi al funzionamento del sistema (eventi di sistema), eventi generati da azioni dell'utente (eventi dell'utente) e così via.

Una possibile gerarchia degli eventi:

trait Event 

trait UserEvent extends Event 

trait SystemEvent extends Event 

trait ApplicationEvent extends SystemEvent 

trait ErrorEvent extends ApplicationEvent 

Ora i programmatori che lavorano sul sistema event-driven hanno bisogno di trovare un modo per registrare/processo gli eventi generati nel sistema. Creeranno un tratto, Sink, che viene utilizzato per contrassegnare i componenti che devono essere avvisati quando un evento è stato attivato.

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

Come conseguenza della marcatura del parametro del tipo con il simbolo -, il tipo di Sink diventato controvariante.

Un modo per informare le parti interessate che un evento è accaduto è scrivere un metodo e passarlo all'evento corrispondente. Questo metodo ipoteticamente fare un po 'di elaborazione e poi si prenderà cura di notificare il sink di evento:

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 paio di implementazioni Sink ipotetiche.

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

Le seguenti chiamate di metodo sono accettati dal compilatore:

appEventFired(new ApplicationEvent {}, ses) 

errorEventFired(new ErrorEvent {}, ges) 

appEventFired(new ApplicationEvent {}, ges) 

Guardando la serie di chiamate si nota che è possibile chiamare un metodo che prevede un Sink[ApplicationEvent] con un Sink[SystemEvent] e anche con una Sink[Event]. Inoltre, è possibile chiamare il metodo in attesa di un Sink[ErrorEvent] con un Sink[Event].

Sostituendo l'invarianza con un vincolo di controvarianza, un Sink[SystemEvent] diventa un sottotipo di Sink[ApplicationEvent]. Pertanto, la contravarianza può anche essere considerata come una relazione "allargante", dal momento che i tipi vengono "allargati" da quelli più specifici a quelli più generici.

Conclusione

Questo esempio è stato descritto in una serie di articoli su varianza trovati sul my blog

Alla fine, penso che aiuta a comprendere anche la teoria dietro di esso ...

+0

migliore risposta qui –

Problemi correlati