Instalação: Vamos assumir que temos um tipo chamado Iterator
que possui um parâmetro de tipo Element
:
interface Iterator<Element> {}
Então nós temos uma interface Iterable
que tem um método que retornará um Iterator
.
// T has an upper bound of Iterator
interface Iterable<T: Iterator> {
getIterator(): T
}
O problema de Iterator
ser genérico é que precisamos fornecer argumentos de tipo.
Uma idéia para resolver isso é "inferir" o tipo do iterador. O pseudo-código a seguir expressa a ideia de que existe uma variável de tipo Element
que é inferida como sendo o argumento de tipo Iterator
:
interface <Element> Iterable<T: Iterator<Element>> {
getIterator(): T
}
E então usamos em algum lugar como este:
class Vec<Element> implements Iterable<VecIterator<Element>> {/*...*/}
Essa definição de Iterable
não usa em Element
nenhum outro lugar, mas meu caso de uso real. Certas funções que usam Iterable
também precisam ser capazes de restringir seus parâmetros para aceitar Iterable
s que retornam apenas certos tipos de iteradores, como um iterador bidirecional, e é por isso que o iterador retornado é parametrizado em vez de apenas o tipo de elemento.
Questões:
- Existe um nome estabelecido para essas variáveis de tipo inferido? E a técnica como um todo? O desconhecimento da nomenclatura específica dificultou a busca desses exemplos dessa natureza ou o aprendizado de recursos específicos do idioma.
- Nem todos os idiomas com genéricos possuem essa técnica; existem nomes para técnicas semelhantes nesses idiomas?
fonte
Respostas:
Não sei se existe um termo específico para esse problema, mas existem três classes gerais de soluções:
E, claro, a solução padrão: continue explicitando todos esses parâmetros.
Evite tipos de concreto.
Você definiu uma
Iterable
interface como: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: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: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 tipoElement
para queT
seja umIterator<Element>
”, sem ter que conhecer nenhum tipo concretoElement
. Isso significa que a expressão de tipoIterator<_>
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:
Então:
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 declararinterface 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: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 exemploMap.Entry
).fonte