Nome da técnica para inferir argumentos de tipo de um parâmetro de tipo?

9

Instalação: Vamos assumir que temos um tipo chamado Iteratorque possui um parâmetro de tipo Element:

interface Iterator<Element> {}

Então nós temos uma interface Iterableque tem um método que retornará um Iterator.

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

O problema de Iteratorser 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 Elementque é 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 Iterablenão usa em Elementnenhum outro lugar, mas meu caso de uso real. Certas funções que usam Iterabletambém precisam ser capazes de restringir seus parâmetros para aceitar Iterables 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?
Levi Morrison
fonte
11
Você pode mostrar algum código que não é compilado que você deseja compilar? Eu acho que isso tornará a questão mais clara.
Sweeper
11
Você também pode escolher um idioma (ou dizer qual idioma está usando e o que significa a sintaxe). Obviamente, não é, por exemplo, C #. Há muitas informações sobre "inferência de tipo" disponíveis na Internet, mas não tenho certeza de que se aplique aqui.
5
Estou implementando genéricos em um idioma, não tentando obter código em nenhum idioma para compilar. Também é uma questão de nomeação e design. É por isso que é um pouco agnóstico. Sem conhecer os termos, fica difícil encontrar exemplos e documentação nos idiomas existentes. Certamente este não é um problema único?
Levi Morrison

Respostas:

2

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 Iterableinterface 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 Tdo 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 Traitsintaxe 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 Iterableinterface 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 Elementpara que Tseja 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 Iterableinterface 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).

amon
fonte