Domanda

Classi di tipi sembrano essere un'ottima possibilità per scrivere funzioni generiche e riutilizzabili in modo molto coerente , modo efficiente ed estensibile. Ma ancora no & Quot; linguaggio tradizionale & Quot; fornisce loro - Al contrario: Concepts , che sono piuttosto analogici idea, sono stati esclusi dal prossimo C ++!

Qual è il ragionamento contro le macchine da scrivere? Apparentemente molte lingue stanno cercando un modo per affrontare problemi simili: .NET ha introdotto vincoli e interfacce generici come IComparable che consentono funzioni come

T Max<T>(T a, T b) where T : IComparable<T> { // }

per operare su tutti i tipi che implementano l'interfaccia.

Scala utilizza invece una combinazione di tratti e il cosiddetto parametri impliciti / limiti di visualizzazione , che vengono automaticamente passati a funzioni generiche.

Ma entrambi i concetti mostrati qui presentano grandi svantaggi: le interfacce sono basate sull'ereditarietà e quindi relativamente lente a causa dell'irregolarità e inoltre non è possibile lasciare che un tipo esistente le implementi.

Se avessimo bisogno di un'astrazione per un Monoid, potremmo benissimo scrivere un'interfaccia e lasciare che i nostri tipi lo implementino, ma tipi incorporati come int non potrebbero mai operare nativamente sulle tue funzioni.

I parametri impliciti invece sono incompatibili con le interfacce / tratti regolari.

Con le classi di tipi, non ci sarebbe un problema (pseudo-codice)

typeclass Monoid of A where
    static operator (+) (x : A, y : A) : A
    static val Zero : A 
end

instance Int of Monoid where
   static operator (+) (x : Int, y : Int) : Int = x + y
   static val Zero : Int = 0
end

Quindi perché non usiamo le classi di tipo? Dopotutto hanno seri svantaggi?

Modifica : non confondere le macchine da scrivere con la tipizzazione strutturale, i modelli C ++ puri o la tipizzazione anatra. Una typeclass viene istanziata esplicitamente dai tipi e non solo soddisfatta per convenzione . Inoltre può portare utili implementazioni e non solo definire un'interfaccia.

È stato utile?

Soluzione

I concetti sono stati esclusi perché il comitato non pensava che li avrebbe fatti in tempo, e perché non erano considerati essenziali per il rilascio. Non è che non credano di essere una buona idea, semplicemente non pensano che l'espressione di loro per C ++ sia matura: http://herbsutter.wordpress.com/2009/07/21/trip-report/

I tipi statici tentano di impedire il passaggio di un oggetto a una funzione, che non soddisfa i requisiti della funzione. In C ++ questo è un grosso problema, perché al momento si accede all'oggetto dal codice, non si può verificare che sia la cosa giusta.

I concetti tentano di impedire il passaggio di un parametro modello, che non soddisfa i requisiti del modello. Ma nel momento in cui il compilatore accede al parametro template, già sta verificando che sia la cosa giusta, anche senza Concetti. Se si tenta di utilizzarlo in un modo che non supporta, viene visualizzato un errore del compilatore [*]. Nel caso di un pesante codice che utilizza il modello potresti ottenere tre schermate piene di parentesi angolari, ma in linea di principio è un messaggio informativo. La necessità di rilevare errori prima di una compilazione non riuscita è meno urgente della necessità di rilevare errori prima di comportamenti indefiniti in fase di esecuzione.

I concetti semplificano la specifica di interfacce modello che funzioneranno su più istanze . Questo è un problema significativo, ma molto meno urgente rispetto alla specifica di interfacce di funzioni che funzioneranno su più chiamate.

In risposta alla tua domanda - qualsiasi dichiarazione formale " implemento questa interfaccia " ha un grosso svantaggio, che richiede che l'interfaccia sia inventata prima dell'implementazione. I sistemi di inferenza del tipo no, ma hanno il grande svantaggio che le lingue in generale non possono esprimere l'intera interfaccia usando i tipi, e quindi potresti avere un oggetto che si deduce essere del tipo corretto, ma che non ha il semantica attribuita a quel tipo. Se la tua lingua si rivolge alle interfacce (in particolare se le abbina alle classi), allora AFAIK devi prendere una posizione qui e scegliere il tuo svantaggio.

[*] Di solito. Vi sono alcune eccezioni, ad esempio il sistema di tipo C ++ attualmente non ti impedisce di utilizzare un iteratore di input come se fosse un iteratore di inoltro. Per questo hai bisogno di tratti iteratori. La digitazione dell'anatra da sola non ti impedisce di passare un oggetto che cammina, nuota e cigola, ma a un attento esame in realtà non fa nessuna di quelle cose come fa un'anatra, ed è sorpreso di apprendere che pensavi che sarebbe ;-)

Altri suggerimenti

Le interfacce non devono necessariamente essere basate sull'ereditarietà ... questa è una decisione di progettazione diversa e separata. La nuova lingua Go ha interfacce, ma non ha ereditarietà, ad esempio: & Quot; un tipo automaticamente soddisfa qualsiasi interfaccia che specifica un sottoinsieme dei suoi metodi " ;, come Go FAQ lo mette. Le riflessioni di Simionato su eredità e interfacce, spinte dalla recente versione di Go, potrebbero vale la pena leggere.

Concordo sul fatto che le macchine da scrivere siano ancora più potenti, essenzialmente perché, come abstract classi di base , ti consentono di specificare ulteriormente il codice utile (definendo un metodo X aggiuntivo in termini di altri per tutti i tipi che altrimenti corrispondono alla base di base ma non definiscono X stessi) - senza il bagaglio di eredità che ABCs (in modo diverso dalle interfacce) quasi inevitabilmente trasportare. Quasi inevitabilmente perché, ad esempio, ABCs di Python " far credere " che coinvolgono l'ereditarietà, in termini di concettualizzazione che offrono ... ma, in realtà, non hanno bisogno di essere basati sull'ereditarietà (molti stanno solo controllando la presenza e la firma di alcuni metodi, proprio come le interfacce di Go).

Per quanto riguarda il motivo per cui un designer linguistico (come Guido, nel caso di Python) sceglierebbe tale " lupi in abiti ovini " come ABC della Python, sulle più semplici macchine da scrivere tipo Haskell che avevo proposto fin dal 2002, è una domanda più difficile a cui rispondere. Dopotutto, non è come se Python avesse una computazione contro il prendere in prestito concetti da Haskell (ad esempio, comprensione delle liste / espressioni del generatore - Python ha bisogno di una dualità qui, mentre Haskell no, perché Haskell è & Quot; lazy < !> quot;). L'ipotesi migliore che posso offrire è che, ormai, l'eredità è così familiare alla maggior parte dei programmatori che la maggior parte dei progettisti di lingue ritiene di poter ottenere un'accettazione più semplice lanciando le cose in quel modo (anche se i progettisti di Go devono essere lodati per non averlo fatto).

Vorrei iniziare in grassetto: Capisco perfettamente la motivazione di averlo e non riesco a capire la motivazione di alcune persone a discuterne ...

Quello che vuoi è polimorfismo ad hoc non virtuale.

  • ad hoc: l'implementazione può variare
  • non virtuale: per motivi prestazionali; invio di compiletime

Il resto è zucchero secondo me.

Il C ++ ha già un polimorfismo ad hoc tramite modelli. & Quot; quot Concetti &; tuttavia chiarirebbe quale tipo di funzionalità polimorfica ad hoc viene utilizzata da quale entità definita dall'utente.

C # semplicemente non ha modo di farlo. Un approccio che non sarebbe non virtuale : se tipi come float implementassero semplicemente qualcosa come " INumeric " oppure " IAddable " (...) saremmo almeno in grado di scrivere un generico min, max, lerp e basato su quel morsetto, maprange, bezier (...). Tuttavia non sarebbe veloce. Non lo vuoi.

Modi per risolvere questo problema: Poiché .NET esegue comunque la compilazione JIT genera anche codice diverso per List<int> rispetto a List<MyClass> (a causa delle differenze di valore e tipi di riferimento), probabilmente non aggiungerebbe un gran sovraccarico per generare anche codice diverso per l'annuncio parti polimorfiche ad hoc. Il linguaggio C # avrebbe solo bisogno di un modo per esprimerlo. Un modo è quello che hai abbozzato.

Un altro modo sarebbe quello di aggiungere vincoli di tipo alla funzione usando una funzione polimorfica ad hoc:

    U SuperSquare<T, U>(T a) applying{ 
         nonvirtual operator (*) T (T, T) 
         nonvirtual Foo U (T)
    }
    {
        return Foo(a * a);
    }

Ovviamente potresti finire con sempre più vincoli durante l'implementazione di Bar che utilizza Foo. Quindi potresti voler un meccanismo per dare un nome a diversi vincoli che usi regolarmente ... Tuttavia, questo è di nuovo zucchero e un modo per avvicinarsi sarebbe usare semplicemente il concetto di typeclass ...

Assegnare un nome a più vincoli è come definire una classe di tipo, ma mi piacerebbe solo guardarlo come una sorta di meccanismo di abbreviazione - zucchero per una raccolta arbitraria di vincoli di tipo di funzione:

    // adhoc is like an interface: it is about collecting signatures
    // but it is not a type: it dissolves during compilation 
    adhoc AMyNeeds<T, U>
    {
         nonvirtual operator (*) T (T, T) 
         nonvirtual Foo U (T)
    } 

    U SuperSquare<T, U>(T a) applying AMyNeeds<T, U>        
    {
        return Foo(a * a);
    }

In qualche posto " main " tutti gli argomenti di tipo sono noti e tutto diventa concreto e può essere compilato insieme.

Ciò che manca ancora è la mancanza di creare diverse implementazioni. Nell'esempio superiore abbiamo appena usato funzioni polimorfiche e fatto sapere a tutti ...

L'implementazione potrebbe quindi seguire la strada dei metodi di estensione - nella loro capacità di aggiungere funzionalità a qualsiasi classe in qualsiasi momento:

 public static class SomeAdhocImplementations
 {
    public nonvirtual int Foo(float x)
    {
        return round(x);
    }
 }

Adesso puoi scrivere:

    int a = SuperSquare(3.0f); // 3.0 * 3.0 = 9.0 rounded should return 9

Il compilatore controlla tutti " non virtuale " funzioni ad hoc, trova sia un operatore float (*) incorporato che un int Foo (float) e quindi è in grado di compilare quella linea.

Il polimorfismo ad hoc ovviamente ha il rovescio della medaglia che devi ricompilare per ogni tipo di tempo di compilazione in modo da inserire le giuste implementazioni. E probabilmente IL non supporta questo essere messo in una dll. Ma forse ci lavorano comunque ...

Non vedo la reale necessità di istanziare un costrutto di classe di tipo. Se qualcosa non dovesse andare a buon fine durante la compilazione, otteniamo gli errori dei vincoli o se quelli erano legati insieme a un & Quot; adhoc & Quot; codeclock il messaggio di errore potrebbe diventare ancora più leggibile.

    MyColor a = SuperSquare(3.0f); 
    // error: There are no ad hoc implementations of AMyNeeds<float, MyColor> 
    // in particular there is no implementation for MyColor Foo(float)

Ma ovviamente anche l'instanciamento di una classe di tipo / " interfaccia ad hoc di polimorfismo " è pensabile. Il messaggio di errore dovrebbe quindi indicare: & Quot; The AMyNeeds constraint of SuperSquare has not been matched. AMyNeeds is available as StandardNeeds : AMyNeeds<float, int> as defined in MyStandardLib & Quot ;. Sarebbe anche possibile mettere l'implementazione in una classe insieme ad altri metodi e aggiungere & Quot; adhoc interface & Quot; all'elenco delle interfacce supportate.

Ma indipendente dal particolare design del linguaggio: non vedo il rovescio della medaglia di aggiungerli in un modo o nell'altro. Salvare le lingue tipizzate staticamente dovrà sempre spingere i confini del potere espressivo, poiché hanno iniziato concedendo troppo poco, che tende ad essere un insieme più piccolo di potere espressivo un normale programmatoresi sarebbe aspettato di essere possibile ...

tldr: sono dalla tua parte. Roba del genere fa schifo nei principali linguaggi tipicamente statici. Haskell mostrò la strada.

  

Qual è il ragionamento contro le macchine da scrivere?

La complessità dell'implementazione per gli autori di compilatori è sempre una preoccupazione quando si considerano le nuove funzionalità del linguaggio. Il C ++ ha già commesso quell'errore e di conseguenza abbiamo già sofferto anni di buggy compilatori C ++.

  

Le interfacce sono basate sull'ereditarietà e quindi relativamente lente a causa dell'irregolarità e inoltre non è possibile lasciare che un tipo esistente le implementi

Non vero. Guarda il sistema di oggetti strutturalmente tipizzato di OCaml, ad esempio:

# let foo obj = obj#bar;;
val foo : < bar : 'a; .. > -> 'a = <fun>

Quella foo funzione accetta qualsiasi oggetto di qualsiasi tipo che fornisce il metodo bar necessario.

Lo stesso vale per il sistema di moduli di ordine superiore di ML. In effetti, esiste persino un'equivalenza formale tra quella e le classi di tipi. In pratica, le classi di tipi sono migliori per le astrazioni su piccola scala come il sovraccarico dell'operatore, mentre i moduli di ordine superiore sono migliori per le astrazioni su larga scala come la parametrizzazione di Okasaki di elenchi catenabili su code.

  

Dopo tutto hanno seri svantaggi?

Guarda il tuo esempio, l'aritmetica generica. F # può effettivamente già gestire quel caso particolare grazie all'interfaccia INumeric. Il tipo F # Matrix usa persino quell'approccio.

Tuttavia, hai appena sostituito il codice macchina per aggiungerlo con invio dinamico a una funzione separata, rendendo più lenti gli ordini aritmetici di grandezza. Per la maggior parte delle applicazioni, ciò è inutilmente lento. È possibile risolvere questo problema eseguendo ottimizzazioni dell'intero programma ma ciò presenta evidenti svantaggi. Inoltre, c'è poca comunanza tra i metodi numerici per int vs float a causa della solidità numerica, quindi anche la tua astrazione è praticamente inutile.

La domanda dovrebbe sicuramente essere: qualcuno può fare un caso convincente per l'adozione di classi di tipo?

  

Ma ancora nessuna "lingua tradizionale" fornisce [tipo di classi.]

Quando è stata posta questa domanda, potrebbe essere vero. Oggi c'è un interesse molto più forte in lingue come Haskell e Clojure. Haskell ha classi di tipi ( class / istanza ), Clojure 1.2+ ha i protocolli ( defprotocol / estende ).

  

Qual è il ragionamento contro [tipo classi]?

Non credo che le classi di tipi siano obiettivamente "peggio" rispetto ad altri meccanismi di polimorfismo; seguono solo un approccio diverso. Quindi la vera domanda è: si adattano bene a un particolare linguaggio di programmazione?

Consideriamo brevemente in che modo le classi di tipi sono diverse dalle interfacce in linguaggi come Java o C #. In questi linguaggi, una classe supporta solo interfacce che sono esplicitamente menzionate e implementate nella definizione di quella classe. Le classi di tipi, tuttavia, sono interfacce che possono essere successivamente aggiunte a qualsiasi tipo già definito, anche in un altro modulo. Questo tipo di estensibilità di tipo è ovviamente abbastanza diverso dai meccanismi in alcuni "tradizionali". Lingue OO.


Consideriamo ora le classi di tipi per alcuni linguaggi di programmazione tradizionali.

Haskell : non c'è bisogno di dire che questa lingua ha classi di tipo .

Clojure : come detto sopra, Clojure ha qualcosa come classi di tipo sotto forma di protocolli .

C ++ : come hai detto tu stesso, concetti sono stati eliminati dalla specifica C ++ 11.

  

Al contrario: i concetti, che sono un'idea abbastanza analogica, sono stati esclusi dal prossimo C ++!

Non ho seguito l'intero dibattito su questa decisione. Da quello che ho letto, i concetti non erano ancora "pronti": c'era ancora un dibattito sulle mappe concettuali. Tuttavia, i concetti non sono stati completamente abbandonati, si prevede che entreranno nella prossima versione di C ++.

C # : con il linguaggio versione 3, C # è diventato essenzialmente un ibrido dei paradigmi di programmazione orientata agli oggetti e funzionale. È stata aggiunta un'aggiunta al linguaggio che è concettualmente abbastanza simile alle classi di tipi: metodi di estensione . La differenza principale è che apparentemente stai collegando nuovi metodi a un tipo esistente, non alle interfacce.

(Certo, il meccanismo del metodo di estensione non è così elegante come l'istanza di Haskell ... dove . I metodi di estensione non sono "veramente" collegati a un tipo, ma sono implementati come una trasformazione sintattica. Alla fine, tuttavia, ciò non fa una grande differenza pratica.)

Non penso che questo accadrà presto - i progettisti del linguaggio probabilmente non aggiungeranno nemmeno l'estensione proprietà al linguaggio e l'estensione interfacce fare un passo oltre.

( VB.NET : Microsoft ha "co-evoluto" i linguaggi C # e VB.NET da qualche tempo, quindi le mie dichiarazioni su C # sono valide anche per VB.NET .)

Java : non conosco molto bene Java, ma delle lingue C ++, C # e Java, è probabilmente il "più puro" Lingua OO. Non vedo come le classi di tipi si adatteranno naturalmente a questa lingua.

F # : ho trovato un post nel forum che spiega perché le classi di tipi potrebbero non essere mai introdotte in F # . Questa spiegazione è incentrata sul fatto che F # ha un sistema di tipo nominativo e non strutturale. (Anche se non sono sicuro se questo sia un motivo sufficiente per F # non avere classi di tipi.)

Prova a definire un Matroid, che è quello che facciamo (logicamente e non dire oralmente un Matroid), ed è probabilmente qualcosa di simile a una C-strutt. Principio di Liskov (ultima medaglia turing) diventa troppo astratta, troppo categorica, troppo teorica, meno curativa dati e un sistema di classe teorico più puro, per una soluzione pragmatica di problemi pratici, li abbiamo brevemente guardati come PROLOG, codice su codice su codice su codice ... mentre un algoritmo descrive sequenze e diari di viaggio che capiamo su carta o lavagna. Dipende dall'obiettivo che hai, risolvendo il problema con un codice minimo o il più astratto.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top