Digitare class vs interfacce oggetto


33

Non credo di capire le classi di tipi. Ho letto da qualche parte che pensare alle classi di tipi come "interfacce" (da OO) che un tipo implementa è sbagliato e fuorviante. Il problema è che sto avendo problemi a vederli come qualcosa di diverso e come ciò sia sbagliato.

Ad esempio, se ho una classe di tipo (nella sintassi di Haskell)

class Functor f where
  fmap :: (a -> b) -> f a -> f b

In che modo differisce dall'interfaccia [1] (nella sintassi Java)

interface Functor<A> {
  <B> Functor<B> fmap(Function<B, A> fn)
}

interface Function<Return, Argument> {
  Return apply(Argument arg);
}

Una possibile differenza che mi viene in mente è che l'implementazione della classe di tipo utilizzata in una determinata chiamata non è specificata ma piuttosto determinata dall'ambiente, ad esempio esaminando i moduli disponibili per un'implementazione per questo tipo. Quello sembra essere un artefatto di implementazione che potrebbe essere affrontato in un linguaggio OO; come il compilatore (o il runtime) potrebbe cercare un wrapper / extender / monkey-patcher che espone l'interfaccia necessaria sul tipo.

Cosa mi sto perdendo?

[1] Nota che l' f aargomento è stato rimosso dal fmapmomento che dato che è un linguaggio OO, avresti chiamato questo metodo su un oggetto. Questa interfaccia presuppone che l' f aargomento sia stato risolto.

Risposte:


46

Nella loro forma di base, le classi di tipi sono in qualche modo simili alle interfacce degli oggetti. Tuttavia, per molti aspetti, sono molto più generali.

  1. La spedizione è sui tipi, non sui valori. Nessun valore è richiesto per eseguirlo. Ad esempio, è possibile inviare il tipo di funzione risultato, come nella Readclasse di Haskell :

    class Read a where
      readsPrec :: Int -> String -> [(a, String)]
      ...
    

    Tale spedizione è chiaramente impossibile nelle OO convenzionali.

  2. Le classi di tipi si estendono naturalmente a più spedizioni, semplicemente fornendo più parametri:

    class Mul a b c where
      (*) :: a -> b -> c
    
    instance Mul Int Int Int where ...
    instance Mul Int Vec Vec where ...
    instance Mul Vec Vec Int where ...
    
  3. Le definizioni di istanza sono indipendenti dalle definizioni di classe e tipo, il che le rende più modulari. Un tipo T dal modulo A può essere adattato a una classe C dal modulo M2 senza modificare la definizione di uno dei due, semplicemente fornendo un'istanza nel modulo M3. In OO, questo richiede funzionalità linguistiche più esoteriche (e meno OO-ish) come i metodi di estensione.

  4. Le classi di tipi si basano sul polimorfismo parametrico, non sul sottotipo. Ciò consente una digitazione più accurata. Considerare ad es

    pick :: Enum a => a -> a -> a
    pick x y = if fromEnum x == 0 then y else x
    

    vs.

    pick(x : Enum, y : Enum) : Enum = if x.fromEnum() == 0 then y else x
    

    Nel primo caso, l'applicazione pick '\0' 'x'ha tipo Char, mentre nel secondo caso, tutto ciò che sapresti sul risultato sarebbe che si tratta di un Enum. (Questo è anche il motivo per cui la maggior parte delle lingue OO oggigiorno integra polimorfismo parametrico.)

  5. Strettamente correlato è il problema dei metodi binari. Sono completamente naturali con le classi di tipi:

    class Ord a where
      (<) :: a -> a -> Bool
      ...
    
    min :: Ord a => a -> a -> a
    min x y = if x < y then x else y
    

    Solo con il sottotipo, l' Ordinterfaccia è impossibile da esprimere. Hai bisogno di una forma più complicata e ricorsiva o di un polimorfismo parametrico chiamato "quantificazione legata all'F" per farlo con precisione. Confronta Java Comparablee il suo utilizzo:

    interface Comparable<T> {
      int compareTo(T y);
    };
    
    <T extends Comparable<T>> T min(T x, T y) {
      if (x.compareTo(y) < 0)
        return x;
      else
        return y;
    }
    

D'altra parte, le interfacce basate sul sottotipo consentono naturalmente la formazione di raccolte eterogenee, ad esempio un elenco di tipi List<C>può contenere membri che hanno vari sottotipi di C(anche se non è possibile ripristinare il loro tipo esatto, se non utilizzando i downcast). Per fare lo stesso in base alle classi di tipi, sono necessari tipi esistenziali come funzionalità aggiuntiva.


Ah, ha molto senso. La spedizione di tipo vs valore-based è probabilmente la cosa più grande a cui non stavo pensando correttamente. La questione del polimorfismo parametrico e della tipizzazione più specifica ha senso. Avevo appena messo insieme quelle interfacce basate sul sottotitolo nella mia mente (apparentemente penso in Java: - /).
oconnor0,

I tipi esistenziali sono simili alla creazione di sottotipi Csenza la presenza di downcasts?
oconnor0,

Tipo. Sono un mezzo per rendere un tipo astratto, cioè nasconderne la rappresentazione. In Haskell, se si associano anche vincoli di classe, è comunque possibile utilizzare i metodi di tali classi su di esso, ma nient'altro. - I downcast sono in realtà una caratteristica separata sia dal sottotipo che dalla quantificazione esistenziale, e in linea di principio potrebbero essere aggiunti anche in presenza di quest'ultimo. Proprio come ci sono lingue OO che non lo forniscono.
Andreas Rossberg,

PS: FWIW, i tipi di caratteri jolly in Java sono tipi esistenziali, anche se piuttosto limitati e ad hoc (che possono essere parte del motivo per cui sono in qualche modo confusi).
Andreas Rossberg,

1
@didierc, che sarebbe limitato ai casi che possono essere risolti completamente staticamente. Inoltre, per abbinare le classi di tipi richiederebbe una forma di risoluzione di sovraccarico che è in grado di distinguere in base al solo tipo di ritorno (vedi elemento 1).
Andreas Rossberg,

6

Oltre all'ottima risposta di Andreas, tieni presente che le classi di tipi hanno lo scopo di semplificare il sovraccarico , che influisce sullo spazio dei nomi globale. Non c'è sovraccarico in Haskell diverso da quello che puoi ottenere tramite le classi di tipi. Al contrario, quando si utilizzano le interfacce degli oggetti, solo le funzioni dichiarate che accettano argomenti di tale interfaccia dovranno preoccuparsi dei nomi delle funzioni in tale interfaccia. Pertanto, le interfacce forniscono spazi dei nomi locali.

Ad esempio, avevi fmapun'interfaccia a oggetti chiamata "Functor". Sarebbe perfettamente giusto averne un altro fmapin un'altra interfaccia, dire "Structor". Ogni oggetto (o classe) può scegliere quale interfaccia desidera implementare. Al contrario, in Haskell, puoi averne solo uno fmapin un determinato contesto. Non è possibile importare classi di tipo Functor e Structor nello stesso contesto.

Le interfacce degli oggetti sono più simili alle firme ML standard che alle classi di tipi.


eppure sembra esserci una stretta relazione tra i moduli ML e le classi di tipo Haskell. cse.unsw.edu.au/~chak/papers/DHC07.html
Steven Shaw

1

Nel tuo esempio concreto (con classe di tipo Functor), le implementazioni di Haskell e Java si comportano diversamente. Immagina di avere forse il tipo di dati e desideri che sia Functor (è un tipo di dati molto popolare in Haskell, che puoi facilmente implementare anche in Java). Nel tuo esempio Java farai implementare forse la tua classe con l'interfaccia di Functor. Quindi puoi scrivere quanto segue (solo codice pseudo perché ho solo lo sfondo c #):

Maybe<Int> val = new Maybe<Int>(5);
Functor<Int> res = val.fmap(someFunctionHere);

Si noti che resha tipo Functor, non forse. Questo rende l'implementazione Java quasi inutilizzabile perché perdi informazioni sul tipo concreto e devi eseguire i cast. (almeno non sono riuscito a scrivere una tale implementazione in cui i tipi erano ancora presenti). Con le classi di tipo Haskell otterrai forse Int di conseguenza.


Penso che questo problema sia dovuto al fatto che Java non supporta tipi di tipo superiore e non è correlato alla discussione sulle interfacce Vs typeclasses. Se Java avesse tipi più alti, allora fmap potrebbe benissimo restituire a Maybe<Int>.
dcastro,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.