2016-05-09 20 views
9

In OOP è buona norma parlare alle interfacce non alle implementazioni. Così, ad esempio, si scrive qualcosa di simile (da Seq intendo scala.collection.immutable.Seq :)):Mancanza corrispondenza impedenza funzionale oggetto

// talk to the interface - good OOP practice 
doSomething[A](xs: Seq[A]) = ??? 
non

qualcosa di simile al seguente:

// talk to the implementation - bad OOP practice 
doSomething[A](xs: List[A]) = ??? 

Tuttavia, in puro linguaggi di programmazione funzionali come Haskell , non si ha il polimorfismo del sottotipo e si usa, invece, il polimorfismo ad hoc attraverso classi di tipi. Quindi, ad esempio, hai il tipo di dati dell'elenco e un'istanza monadica per l'elenco. Non devi preoccuparti di usare un'interfaccia/classe astratta perché non hai un tale concetto.

Nei linguaggi ibridi, come Scala, si hanno entrambe le classi di tipi (attraverso un modello, in realtà, e non cittadini di prima classe come in Haskell, ma divagando) e il sottotipo polimorfismo. In scalaz, cats e così via si hanno istanze monadiche per tipi concreti, non per quelli astratti, ovviamente.

Infine la domanda: dato questo ibridismo di Scala si fa comunque rispettare la regola OOP di parlare con interfacce o semplicemente parlare con tipi concreti per approfittare di funtori, monadi e così via direttamente senza dover convertire in cemento scrivi ogni volta che devi usarli? In altre parole, è ancora valido in Scala parlare con le interfacce anche se si desidera abbracciare FP anziché OOP? In caso contrario, che cosa succede se hai scelto di utilizzare List e, in seguito, ti sei reso conto che una Vector sarebbe stata una scelta migliore?

P.S .: Nei miei esempi ho utilizzato un metodo semplice, ma lo stesso ragionamento si applica ai tipi definiti dall'utente. Es .:

+0

C'è anche .. 'def doQualcosa [A, M [X] <: Seq [X]] (xs: M [A]) = ???' –

+1

Questa domanda potrebbe essere più adatta per [Programmatori SE] (http://programmers.stackexchange.com/). I motivi principali per parlare con le interfacce è 1) potresti voler sostituire un'implementazione con un'altra e 2) in modo da poter isolare i tuoi tipi l'uno dall'altro quando si scrivono i test delle unità. Questi problemi non si applicano ai tipi "primitivi", come ints o string - non c'è interfaccia per 'int' o' string'. – dcastro

+0

Ritengo che la maggior parte delle monadi siano primitive che ti aiutano a incollare il tuo codice. Pertanto, non c'è bisogno di interfacce. – dcastro

risposta

1

Quello che vorrei attaccare qui è il vostro concetto "concreto contro interfaccia". Guardalo in questo modo: ogni tipo ha un'interfaccia, nel senso generale del termine "interfaccia". Un tipo "concreto" è solo un caso limite.

Quindi diamo un'occhiata alle liste Haskell da questa angolazione. Qual è l'interfaccia di una lista? Beh, le liste sono un algebrica tipo di dati, e tutti questi tipi di dati hanno la stessa forma generale di interfaccia e di contratto:

  1. È possibile costruire istanze del tipo usando i suoi costruttori in base alle loro arietà e argomento tipi;
  2. È possibile osservare le istanze del tipo confrontandosi con i loro costruttori in base alle loro origini e tipi di argomenti;
  3. La costruzione e l'osservazione sono inversi: quando si esegue lo schema di corrispondenza con un valore, ciò che si ottiene è esattamente ciò che è stato inserito.

Se si guarda in questi termini, credo che la seguente regola funziona piuttosto bene in entrambe le paradigma:

  • scegliere i tipi le cui interfacce e contratti corrispondere esattamente con le vostre esigenze.
    • Se il loro contratto è più debole delle vostre esigenze, allora non manterranno invarianti di cui avete bisogno;
    • Se i loro contratti sono più forti dei tuoi requisiti, potresti involontariamente abbinarti ai dettagli "extra" e limitare la possibilità di modificare il programma in un secondo momento.

Quindi vi chiedo non è più se un tipo è "concreta" o "astratto" -solo se si adatta alle vostre esigenze.

+1

È non in questo modo che funziona in OOP. In Haskell hai tipi di dati (ADT). Hai entrambe le liste (liste collegate) e i vettori (matrici int-indexed) ma non condividono un _supertype_ comune. Se la tua funzione prende una lista non puoi passarla a un vettore. In Java/Scala hai il polimorfismo del sottotipo e non devi preoccuparti se il codice client passa una lista o un vettore, basta prendere un Seq (List in Java) e il gioco è fatto. Inoltre, in OOP è buona pratica prendere Seq, non List o Vector, dal momento che Seq soddisfa esattamente le tue esigenze in modo da non doverti preoccupare dell'implementazione concreta inoltrata. – lambdista

+0

Direi che l'equivalente delle interfacce oop in questo caso è essere considerato il typeclass di haskell. L'astrazione in esame riguarda la scrittura di codice generico che funziona utilizzando il contratto minimo necessario per i tuoi dati. –

+0

@lambdista: ci sono semplici funzioni che convertono tra liste e vettori. Poiché gli elenchi di Haskell sono pigri, operativamente questo è molto simile agli iteratori/'Seq'. Quindi, il problema è risolto. OOP si preoccupa troppo delle relazioni IS-A quando CAN-BE-TURNED-INTO-A funziona già molto bene. –

0

Questi sono i miei due centesimi su questo argomento. In Haskell hai tipi di dati (ADT). Hai sia liste (liste collegate) che vettori (matrici int-indexed) ma non condividono un supertipo comune. Se la tua funzione prende una lista non puoi passarla a un vettore.

In Scala, essendo un linguaggio ibrido OOP-FP, si dispone di sottotipo il polimorfismo troppo in modo da non importa se il codice del client passa una List o un Vector, solo richiedono un Seq (possibilmente immutabile) e il gioco è fatto .

Credo che per rispondere a questa domanda è necessario porsi un'altra domanda: "Voglio abbracciare FP in toto?". Se la risposta è sì, non utilizzare Seq o qualsiasi altra superclasse astratta nel senso OOP. Ovviamente, l'eccezione a questa regola è l'uso di una classe trait/abstract quando si definiscono gli ADT in Scala. Ad esempio:

sealed trait Tree[+A] 
case object Empty extends Tree[Nothing] 
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A] 

In questo caso sarebbe necessario disporre Tree[A] come un tipo, naturalmente, e quindi utilizzare, per esempio, pattern matching per determinare se è o Empty o Node[A].

Immagino che la mia opinione su questo argomento sia confermata dal libro rosso (Functional Programming in Scala). Non usano mai Seq, ma List, Vector e così via. Inoltre, gli haskeller non si preoccupano di questi problemi e usano gli elenchi ogni volta che hanno bisogno di una semantica e di vettori di liste collegate ogni volta che hanno bisogno della semantica int-indexed-array.

Se, d'altra parte, si vuole abbracciare l'OOP e utilizzare Scala come meglio Java poi su OK, si dovrebbe seguire la procedura consigliata per OOP parlare alle interfacce di non implementazioni.

Se stai pensando: "Preferirei optare per per lo più funzionale", quindi dovresti leggere Erik Meijer's The Curse of the Excluded Middle.