Nome della tecnica per inferire argomenti tipo di un parametro di tipo?

9

Setup: supponiamo di avere un tipo chiamato Iterator che ha un parametro di tipo Element :

interface Iterator<Element> {}

Quindi abbiamo un'interfaccia Iterable che ha un metodo che restituirà un Iterator .

// T has an upper bound of Iterator
interface Iterable<T: Iterator> {
    getIterator(): T
}

Il problema con Iterator generico è che dobbiamo fornirlo con argomenti tipo.

Un'idea per risolvere questo è "inferire" il tipo di iteratore. Il seguente pseudo-codice esprime l'idea che esiste una variabile di tipo Element che viene dedotta come argomento di tipo a Iterator :

interface <Element> Iterable<T: Iterator<Element>> {
    getIterator(): T
}

E poi lo usiamo da qualche parte in questo modo:

class Vec<Element> implements Iterable<VecIterator<Element>> {/*...*/}

Questa definizione di Iterable non usa Element in nessun altro punto della sua definizione, ma il mio caso d'uso reale sì. Alcune funzioni che usano Iterable devono anche essere in grado di vincolare i loro parametri per accettare Iterable s che restituiscono solo determinati tipi di iteratori, come un iteratore bidirezionale, che è il motivo per cui l'iteratore restituito è parametrizzato invece del solo tipo di elemento .

Domande:

  • Esiste un nome stabilito per queste variabili di tipo inferito? Che dire della tecnica nel suo insieme? Non conoscendo la nomenclatura specifica ha reso difficile la ricerca di questi esempi di questo in the wild o informazioni sulle funzionalità specifiche della lingua.
  • Non tutte le lingue con generici hanno questa tecnica; ci sono nomi per le tecniche simili in queste lingue?
posta Levi Morrison 09.07.2018 - 18:37
fonte

1 risposta

2

Non so se c'è un termine particolare per questo problema, ma ci sono tre classi generali di soluzioni:

  • evita tipi concreti a favore dell'invio dinamico
  • consente di impostare i parametri del tipo segnaposto nei vincoli di tipo
  • evitare i parametri di tipo usando tipi / tipi di famiglie associati

E ovviamente la soluzione predefinita: continua a scrivere tutti quei parametri.

Evita tipi di calcestruzzo.

Hai definito un'interfaccia Iterable come:

interface <Element> Iterable<T: Iterator<Element>> {
    getIterator(): T
}

Questo fornisce agli utenti dell'interfaccia potenza massima in quanto ottengono il tipo concreto esatto T dell'iteratore. Ciò consente anche a un compilatore di applicare più ottimizzazioni come l'inlining.

Tuttavia, se Iterator<E> è un'interfaccia inviata dinamicamente, non è necessario conoscere il tipo concreto. Questo è ad es. la soluzione utilizzata da Java. L'interfaccia verrebbe quindi scritta come:

interface Iterable<Element> {
    getIterator(): Iterator<Element>
}

Una variante interessante di questo è la sintassi di impl Trait di Rust che ti permette di dichiarare la funzione con un tipo di ritorno astratto, ma sapendo che il tipo concreto sarà conosciuto nel sito di chiamata (consentendo così ottimizzazioni). Ciò si comporta in modo simile a un parametro di tipo implicito.

Consenti parametri tipo segnaposto.

L'interfaccia Iterable non ha bisogno di conoscere il tipo di elemento, quindi potrebbe essere possibile scrivere questo come:

interface Iterable<T: Iterator<_>> {
    getIterator(): T
}

Dove T: Iterator<_> esprime il vincolo "T è un qualsiasi iteratore, indipendentemente dal tipo di elemento". Più rigorosamente, possiamo esprimere questo come: "esiste qualche tipo Element in modo che T sia un Iterator<Element> ", senza dover conoscere alcun tipo di calcestruzzo per Element . Ciò significa che l'espressione di tipo Iterator<_> non descrive un tipo effettivo e può essere utilizzata solo come un vincolo di tipo.

Utilizza famiglie di tipi / tipi associati.

es. in C ++, un tipo può avere membri di tipo. Questo è comunemente usato in tutta la libreria standard, ad es. %codice%. Questo in realtà non risolve il problema del parametro di tipo in tutti gli scenari, ma poiché un tipo può riferirsi ad altri tipi, un singolo parametro di tipo può descrivere un'intera famiglia di tipi correlati.

Definiamo:

interface Iterator {
  type ElementType
  fn next(): ElementType
}

interface Iterable {
  type IteratorType: Iterator
  fn getIterator(): IteratorType
}

Quindi:

class Vec<Element> implement Iterable {
  type IteratorType = VecIterator<Element>
  fn getIterator(): IteratorType { ... }
}

class VecIterator<T> implements Iterator {
  type ElementType = T
  fn next(): ElementType { ... }
}

Questo sembra molto flessibile, ma si noti che questo può rendere più difficile esprimere vincoli di tipo. Per esempio. come scritto std::vector::value_type non impone alcun tipo di elemento iteratore, e potremmo invece voler dichiarare Iterable . E ora hai a che fare con un calcolo di tipo abbastanza complesso. È molto facile rendere un sistema di questo tipo indecidibile (o forse lo è già?).

Si noti che i tipi associati possono essere molto convenienti come valori predefiniti per i parametri del tipo. Per esempio. partendo dal presupposto che l'interfaccia interface Iterator<T> abbia bisogno di un parametro di tipo separato per il tipo di elemento che è solitamente ma non sempre uguale al tipo di elemento iteratore e che abbiamo parametri di tipo segnaposto, è possibile dire:

interface Iterable<T: Iterator<_>, Element = T::Element> {
  ...
}

Tuttavia, questa è solo una funzione di ergonomia del linguaggio e non rende il linguaggio più potente.

I sistemi di tipo sono difficili, quindi è bene dare un'occhiata a cosa funziona e cosa non funziona in altre lingue.

es. considera la lettura del capitolo Tratti avanzati nel Rust Book, che discute i tipi associati. Ma si noti che alcuni punti a favore dei tipi associati invece dei generici si applicano solo in quanto la lingua non presenta sottotipi e ogni tratto può essere implementato al massimo una volta per tipo. Cioè I tratti ruggine non sono interfacce tipo Java.

Altri sistemi di tipi interessanti includono Haskell con varie estensioni di lingua. OCaml modules / functors sono una versione relativamente semplice di famiglie di tipi, senza mescolarli direttamente con oggetti o tipi parametrizzati. Java è notevole per le limitazioni nel suo sistema di tipi, ad es. generici con cancellazione dei tipi e niente generici su tipi di valore. C # è molto simile a Java ma riesce a evitare la maggior parte di queste limitazioni, a costo di una maggiore complessità di implementazione. Scala cerca di integrare i generici in stile C # con i typeclass in stile Haskell sulla piattaforma Java. I modelli ingannevolmente semplici di C ++ sono ben studiati ma sono diversi dalla maggior parte delle implementazioni generiche.

Vale anche la pena guardare le librerie standard di questi linguaggi (in particolare raccolte di librerie standard come elenchi o tabelle hash) per vedere quali modelli sono comunemente usati. Per esempio. C ++ ha un sistema complesso di diverse capacità di iteratore e Scala codifica le funzionalità di raccolta a grana fine come tratti. Le interfacce della libreria standard Java sono talvolta non corrette, ad es. Iterable , ma è possibile utilizzare classi nidificate come tipo di tipo associato (ad esempio Iterator#remove() ).

    
risposta data 13.07.2018 - 15:23
fonte