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


9

Setup: supponiamo di avere un tipo chiamato Iteratorche ha un parametro type Element:

interface Iterator<Element> {}

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

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

Il problema con l' Iteratoressere generico è che dobbiamo fornirlo con argomenti di tipo.

Un'idea per risolvere questo è di "inferire" il tipo di iteratore. Il seguente pseudo-codice esprime l'idea che esiste una variabile di tipo Elementche 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 Iterablenon usa Elementaltrove nella sua definizione, ma il mio vero caso d'uso lo fa. Alcune funzioni che utilizzano Iterabledevono anche essere in grado di vincolare i propri parametri per accettare messaggi Iterableche restituiscono solo determinati tipi di iteratori, come un iteratore bidirezionale, motivo per cui l'iteratore restituito viene parametrizzato anziché solo il tipo di elemento.


Domande:

  • Esiste un nome stabilito per queste variabili di tipo dedotte? E la tecnica nel suo insieme? Non conoscere una nomenclatura specifica ha reso difficile la ricerca di questi esempi allo stato brado o la conoscenza di caratteristiche specifiche della lingua.
  • Non tutte le lingue con generici hanno questa tecnica; ci sono nomi per tecniche simili in queste lingue?

1
Puoi mostrare del codice che non si compila che si desidera compilare? Penso che ciò renderà la domanda più chiara.
Spazzatrice

1
Puoi anche scegliere una lingua (o dire quale lingua stai usando e cosa significa la sintassi). Ovviamente non è, ad esempio, C #. Ci sono molte informazioni su "inferenza di tipo" disponibili su Internet, ma non sono sicuro che si applichi qui.

5
Sto implementando generici in una lingua, non sto cercando di ottenere il codice in nessuna lingua da compilare. È anche una domanda di denominazione e design. Questo è il motivo per cui è in qualche modo agnostico. Senza conoscere i termini, è difficile trovare esempi e documentazione nelle lingue esistenti. Sicuramente questo non è un problema unico?
Levi Morrison,

Risposte:


2

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

  • evitare tipi concreti a favore della spedizione dinamica
  • consentire i parametri di tipo segnaposto nei vincoli di tipo
  • evitare i parametri di tipo utilizzando tipi / famiglie di tipi associati

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

Evita i tipi concreti.

Hai definito Iterableun'interfaccia come:

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

Ciò offre agli utenti dell'interfaccia la massima potenza perché ottengono l'esatto tipo concreto Tdell'iteratore. Ciò consente inoltre a un compilatore di applicare più ottimizzazioni come l'inline.

Tuttavia, se si Iterator<E>tratta di un'interfaccia spedita in modo dinamico, non è necessario conoscere il tipo di calcestruzzo. Questa è ad esempio la soluzione utilizzata da Java. L'interfaccia verrebbe quindi scritta come:

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

Una variante interessante di questo è la impl Traitsintassi di Rust che consente di dichiarare la funzione con un tipo di ritorno astratto, ma sapendo che il tipo concreto sarà noto nel sito di chiamata (consentendo quindi ottimizzazioni). Ciò si comporta in modo simile a un parametro di tipo implicito.

Consenti parametri del tipo di segnaposto.

L' Iterableinterfaccia 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 iteratore, indipendentemente dal tipo di elemento". Più rigorosamente, possiamo esprimerlo come: "esiste un tipo in Elementmodo che Tsia un Iterator<Element>", senza dover conoscere alcun tipo concreto per Element. Ciò significa che l'espressione di tipo Iterator<_>non descrive un tipo reale e può essere utilizzata solo come vincolo di tipo.

Usa famiglie di tipi / tipi associati.

Ad esempio in C ++, un tipo può avere membri del tipo. Questo è comunemente usato in tutta la libreria standard, ad es std::vector::value_type. Questo non risolve davvero il problema dei parametri di tipo in tutti gli scenari, ma poiché un tipo può fare riferimento 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
}

Poi:

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

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

Sembra molto flessibile, ma nota che ciò può rendere più difficile esprimere i vincoli di tipo. Ad esempio, come scritto Iterablenon impone alcun tipo di elemento iteratore e potremmo voler dichiarare interface Iterator<T>invece. E ora hai a che fare con un calcolo di tipo abbastanza complesso. È molto facile rendere indecidibile un sistema di questo tipo (o forse lo è già?).

Si noti che i tipi associati possono essere molto utili come valori predefiniti per i parametri di tipo. Ad esempio, supponendo che l' Iterableinterfaccia abbia bisogno di un parametro di tipo separato per il tipo di elemento che di solito è ma non sempre uguale al tipo di elemento iteratore e che abbiamo parametri di tipo segnaposto, potrebbe essere possibile dire:

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

Tuttavia, questa è solo una funzione di ergonomia della lingua e non rende la lingua più potente.


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

Ad esempio, considera di leggere il capitolo Tratti avanzati nel Rust Book, che discute i tipi associati. Tuttavia, tieni presente che alcuni punti a favore di tipi associati anziché generici si applicano solo perché la lingua non presenta sottotipi e ogni tratto può essere implementato al massimo una sola volta per tipo. I tratti di Rust non sono interfacce simili a Java.

Altri sistemi di tipo interessanti includono Haskell con varie estensioni di lingua. I moduli / funzioni OCaml sono una versione relativamente semplice delle famiglie di tipi, senza mescolarle direttamente con oggetti o tipi parametrizzati. Java si distingue per le limitazioni nel suo sistema di tipi, ad es. Generici con cancellazione del tipo e nessun generico rispetto ai tipi di valore. C # è molto simile a Java ma riesce a evitare la maggior parte di questi limiti, a costo di una maggiore complessità dell'implementazione. Scala cerca di integrare i generici in stile C # con le macchine da scrivere 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 le raccolte di librerie standard come elenchi o tabelle hash) per vedere quali schemi sono comunemente usati. Ad esempio, C ++ ha un sistema complesso di diverse funzionalità di iteratore e Scala codifica le funzionalità di raccolta a grana fine come tratti. Le interfacce della libreria standard Java a volte non sono corrette, ad esempio Iterator#remove(), ma possono utilizzare le classi nidificate come un tipo di tipo associato (ad esempio Map.Entry).

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.