Konfiguracja: Załóżmy, że mamy typ o nazwie, Iterator
który ma parametr typu Element
:
interface Iterator<Element> {}
Następnie mamy interfejs, Iterable
który ma jedną metodę, która zwróci Iterator
.
// T has an upper bound of Iterator
interface Iterable<T: Iterator> {
getIterator(): T
}
Problem z Iterator
byciem ogólnym jest taki, że musimy dostarczyć mu argumenty typu.
Jednym ze sposobów rozwiązania tego jest „wywnioskowanie” typu iteratora. Poniższy pseudo-kod wyraża koncepcję, że istnieje zmienna typu, Element
która ma być argumentem typu Iterator
:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
A potem używamy go w taki sposób:
class Vec<Element> implements Iterable<VecIterator<Element>> {/*...*/}
Ta definicja Iterable
nie używa Element
nigdzie indziej w swojej definicji, ale mój prawdziwy przypadek użycia tak. Niektóre funkcje, które używają, Iterable
muszą również mieć możliwość ograniczenia swoich parametrów do akceptacji Iterable
s, które zwracają tylko niektóre rodzaje iteratorów, takich jak iterator dwukierunkowy, dlatego zwrócony iterator jest parametryzowany zamiast tylko typu elementu.
Pytania:
- Czy istnieje ustalona nazwa dla tych wywnioskowanych zmiennych typu? Co z techniką jako całością? Brak znajomości konkretnej nomenklatury utrudnił wyszukiwanie tego przykładów na wolności lub poznanie funkcji specyficznych dla języka.
- Nie wszystkie języki z generycznymi mają tę technikę; czy istnieją nazwy podobnych technik w tych językach?
źródło
Odpowiedzi:
Nie wiem, czy istnieje konkretny termin na ten problem, ale istnieją trzy ogólne klasy rozwiązań:
I oczywiście domyślne rozwiązanie: wciąż przeliteruj wszystkie te parametry.
Unikaj konkretnych rodzajów.
Zdefiniowałeś
Iterable
interfejs jako:Daje to użytkownikom interfejsu maksymalną moc, ponieważ otrzymują dokładnie konkretny typ
T
iteratora. Pozwala to również kompilatorowi na zastosowanie większej liczby optymalizacji, takich jak wstawianie.Jeśli jednak
Iterator<E>
jest to interfejs dynamicznie wywoływany, znajomość konkretnego typu nie jest konieczna. Jest to np. Rozwiązanie, którego używa Java. Interfejs byłby wówczas zapisany jako:Interesującą odmianą tego jest
impl Trait
składnia Rust, która pozwala zadeklarować funkcję za pomocą abstrakcyjnego typu zwracanego, ale wiedząc, że konkretny typ będzie znany w witrynie wywołania (umożliwiając w ten sposób optymalizację). Zachowuje się podobnie do parametru typu niejawnego.Zezwalaj na parametry typu symbolu zastępczego.
Iterable
Interfejs nie musi wiedzieć o rodzaju elementu, więc może to być możliwe, aby napisać to jako:Gdzie
T: Iterator<_>
wyraża ograniczenie „T jest dowolnym iteratorem, niezależnie od typu elementu”. Bardziej rygorystycznie, możemy to wyrazić w następujący sposób: „istnieje jakiś typ,Element
więcT
jest toIterator<Element>
”, bez konieczności poznawania konkretnego typuElement
. Oznacza to, że wyrażenie typuIterator<_>
nie opisuje rzeczywistego typu i może być użyte jedynie jako ograniczenie typu.Użyj rodzin typów / powiązanych typów.
Np. W C ++ typ może mieć członków typu. Jest to powszechnie stosowane w całej standardowej bibliotece, np
std::vector::value_type
. To tak naprawdę nie rozwiązuje problemu parametru typu we wszystkich scenariuszach, ale ponieważ typ może odnosić się do innych typów, pojedynczy parametr typu może opisywać całą rodzinę powiązanych typów.Zdefiniujmy:
Następnie:
Wygląda to bardzo elastycznie, ale należy pamiętać, że może to utrudnić wyrażenie ograniczeń typu. Np. Jak napisano
Iterable
, nie wymusza żadnego typu elementu iteratora iinterface Iterator<T>
zamiast tego możemy chcieć zadeklarować . A teraz masz do czynienia z dość złożonym rachunkiem typu. Bardzo łatwo jest przypadkowo sprawić, że taki typ systemu jest nierozstrzygalny (a może już jest?).Zauważ, że skojarzone typy mogą być bardzo wygodne jako domyślne dla parametrów typu. Np. Zakładając, że
Iterable
interfejs potrzebuje osobnego parametru typu dla typu elementu, który zwykle, ale nie zawsze jest taki sam jak typ elementu iteratora, i że mamy parametry typu zastępczego, można by powiedzieć:Jest to jednak tylko funkcja ergonomii języka i nie czyni języka mocniejszym.
Systemy typów są trudne, więc dobrze jest spojrzeć na to, co działa i nie działa w innych językach.
Np. Zastanów się nad rozdziałem Zaawansowane cechy w Rust Book, który omawia powiązane typy. Należy jednak pamiętać, że niektóre punkty na rzecz powiązanych typów zamiast generycznych mają zastosowanie tylko tam, ponieważ język nie zawiera podtytułów, a każda cecha może zostać zaimplementowana maksymalnie raz dla każdego typu. Tj. Rust nie są interfejsami podobnymi do Javy.
Inne interesujące systemy typów obejmują Haskell z różnymi rozszerzeniami językowymi. Moduły / funktory OCaml są stosunkowo prostą wersją rodzin typów, bez bezpośredniego łączenia ich z obiektami lub sparametryzowanymi typami. Java jest godna uwagi ze względu na ograniczenia w swoim systemie typów, np. Generyczne z usuwaniem typów i brak generycznych względem typów wartości. C # jest bardzo podobny do Javy, ale udaje mu się uniknąć większości z tych ograniczeń, kosztem zwiększonej złożoności implementacji. Scala próbuje zintegrować generyczne style w stylu C # z typami klas Haskell na platformie Java. Zwodniczo proste szablony C ++ są dobrze zbadane, ale w przeciwieństwie do większości implementacji generycznych.
Warto również przyjrzeć się standardowym bibliotekom tych języków (zwłaszcza standardowym kolekcjom bibliotek, takim jak listy lub tabele skrótów), aby zobaczyć, które wzorce są często używane. Np. C ++ ma złożony system różnych możliwości iteracji, a Scala koduje funkcje dokładnego zbierania jako cechy. Standardowe interfejsy biblioteczne Java są czasami niesłyszalne,
Iterator#remove()
ale mogą wykorzystywać klasy zagnieżdżone jako rodzaj powiązanego typu (npMap.Entry
.).źródło