Não sei se existe um termo específico para esse problema, mas existem três classes gerais de soluções:
- evitar tipos concretos a favor de expedição dinâmica
- permitir parâmetros de tipo de espaço reservado em restrições de tipo
- evite parâmetros de tipo usando tipos / famílias de tipos associados
E, claro, a solução padrão: continue explicitando todos esses parâmetros.
Evite tipos de concreto.
Você definiu uma Iterable
interface como:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
Isso fornece aos usuários da interface o máximo de potência, porque eles obtêm o tipo concreto exato T
do iterador. Isso também permite que um compilador aplique mais otimizações, como inlining.
No entanto, se Iterator<E>
é uma interface despachada dinamicamente, não é necessário conhecer o tipo de concreto. Esta é, por exemplo, a solução que Java usa. A interface seria então escrita como:
interface Iterable<Element> {
getIterator(): Iterator<Element>
}
Uma variação interessante disso é a impl Trait
sintaxe de Rust, que permite declarar a função com um tipo de retorno abstrato, mas sabendo que o tipo concreto será conhecido no site da chamada (permitindo otimizações). Isso se comporta de maneira semelhante a um parâmetro de tipo implícito.
Permitir parâmetros de tipo de espaço reservado.
A Iterable
interface não precisa saber sobre o tipo de elemento, portanto, pode ser possível escrever isso como:
interface Iterable<T: Iterator<_>> {
getIterator(): T
}
Onde T: Iterator<_>
expressa a restrição "T é qualquer iterador, independentemente do tipo de elemento". Mais rigorosamente, podemos expressar isso da seguinte maneira: “existe algum tipo Element
para que T
seja um Iterator<Element>
”, sem ter que conhecer nenhum tipo concreto Element
. Isso significa que a expressão de tipo Iterator<_>
não descreve um tipo real e só pode ser usada como restrição de tipo.
Use famílias de tipos / tipos associados.
Por exemplo, em C ++, um tipo pode ter membros do tipo. Isso é comumente usado em toda a biblioteca padrão, por exemplo std::vector::value_type
. Isso realmente não resolve o problema do parâmetro de tipo em todos os cenários, mas como um tipo pode se referir a outros tipos, um único parâmetro de tipo pode descrever toda uma família de tipos relacionados.
Vamos definir:
interface Iterator {
type ElementType
fn next(): ElementType
}
interface Iterable {
type IteratorType: Iterator
fn getIterator(): IteratorType
}
Então:
class Vec<Element> implement Iterable {
type IteratorType = VecIterator<Element>
fn getIterator(): IteratorType { ... }
}
class VecIterator<T> implements Iterator {
type ElementType = T
fn next(): ElementType { ... }
}
Isso parece muito flexível, mas observe que isso pode dificultar a expressão de restrições de tipo. Por exemplo, conforme escrito Iterable
, não impõe nenhum tipo de elemento iterador, e podemos declarar interface Iterator<T>
. E agora você está lidando com um cálculo de tipo bastante complexo. É muito fácil tornar acidentalmente esse tipo de sistema indecidível (ou talvez já seja?).
Observe que os tipos associados podem ser muito convenientes como padrões para os parâmetros de tipo. Por exemplo, supondo que a Iterable
interface precise de um parâmetro de tipo separado para o tipo de elemento que geralmente é, mas nem sempre é o mesmo que o tipo de elemento iterador, e que temos parâmetros de tipo de espaço reservado, pode ser possível dizer:
interface Iterable<T: Iterator<_>, Element = T::Element> {
...
}
No entanto, esse é apenas um recurso de ergonomia da linguagem e não torna a linguagem mais poderosa.
Os sistemas de tipos são difíceis, por isso é bom dar uma olhada no que funciona e no que não funciona em outros idiomas.
Por exemplo, considere a leitura do capítulo Advanced Traits no Rust Book, que discute os tipos associados. Mas observe que alguns pontos a favor dos tipos associados, em vez dos genéricos, só se aplicam a ele porque o idioma não possui subtipagem e cada característica só pode ser implementada no máximo uma vez por tipo. Ou seja, os traços de ferrugem não são interfaces semelhantes a Java.
Outros sistemas de tipos interessantes incluem Haskell com várias extensões de idioma. Os módulos / functores do OCaml são uma versão comparativamente simples das famílias de tipos, sem os misturar diretamente com objetos ou tipos com parâmetros. Java é notável pelas limitações em seu sistema de tipos, por exemplo, genéricos com apagamento de tipo e nenhum genérico sobre tipos de valor. O C # é muito parecido com Java, mas consegue evitar a maioria dessas limitações, ao custo de uma maior complexidade de implementação. O Scala tenta integrar os genéricos no estilo C # às classes de tipo no estilo Haskell na parte superior da plataforma Java. Os modelos enganosamente simples do C ++ são bem estudados, mas diferem da maioria das implementações genéricas.
Também vale a pena examinar as bibliotecas padrão dessas linguagens (especialmente coleções de bibliotecas padrão, como listas ou tabelas de hash) para ver quais padrões são comumente usados. Por exemplo, o C ++ possui um sistema complexo de diferentes recursos do iterador e o Scala codifica recursos de coleção refinados como características. As interfaces da biblioteca padrão Java às vezes não são confiáveis, por exemplo Iterator#remove()
, mas podem usar classes aninhadas como um tipo de tipo associado (por exemplo Map.Entry
).