Um bom sistema de tipos genéricos


29

É geralmente aceito que os genéricos Java falharam em alguns aspectos importantes. A combinação de curingas e limites levou a algum código seriamente ilegível.

No entanto, quando olho para outras linguagens, realmente não consigo encontrar um sistema de tipos genéricos com o qual os programadores estejam satisfeitos.

Se considerarmos o seguinte como objetivos de design de um sistema desse tipo:

  • Sempre produz declarações do tipo de fácil leitura
  • Fácil de aprender (não há necessidade de retocar covariância, contravariância etc.)
  • maximiza o número de erros em tempo de compilação

Existe algum idioma que acertou? Se eu pesquisar no Google, a única coisa que vejo são reclamações sobre como o sistema de tipos é péssimo na linguagem X. Esse tipo de complexidade é inerente à digitação genérica? Devemos desistir de tentar verificar a segurança do tipo 100% em tempo de compilação?

Minha principal pergunta é qual é a linguagem que "acertou" a melhor em relação a esses três objetivos. Percebo que isso é subjetivo, mas até agora não consigo nem encontrar uma linguagem em que nem todos os programadores concordem que o sistema de tipos genéricos é uma bagunça.

Adendo: como observado, a combinação de subtipo / herança e genéricos é o que cria a complexidade. Por isso, estou realmente procurando por uma linguagem que combine ambos e evite a explosão da complexidade.


2
Como assim easy-to-read type declarations? O terceiro critério também é ambíguo: por exemplo, eu posso transformar exceções de índice fora dos limites em erros de tempo de compilação, não permitindo indexar matrizes, a menos que eu possa calcular o índice em tempo de compilação. Além disso, o segundo critério exclui a subtipagem. Isso não é necessariamente uma coisa ruim, mas você deve estar ciente do que está pedindo.
Doval


9
@gnat, isso definitivamente não é um discurso retórico contra Java. Eu programa quase exclusivamente em Java. Meu argumento é que geralmente é aceito na comunidade Java que os genéricos são falhos (não uma falha total, mas provavelmente uma parcial), portanto, é uma pergunta lógica perguntar como eles deveriam ter sido implementados. Por que eles estão errados e os outros acertaram? Ou é realmente impossível obter genéricos absolutamente certos?
Peter

1
Se todo mundo estivesse roubando o C #, haveria menos reclamações. Especialmente Java está em posição de recuperar o atraso copiando. Em vez disso, eles decidem por soluções inferiores. Muitas das perguntas que os comitês de design Java ainda discutem já foram decididas e implementadas em C #. Eles nem parecem olhar.
usr

2
@emodendroket: Eu acho que minhas duas maiores reclamações sobre genéricos C # são que não há como aplicar uma restrição de "supertipo" (por exemplo Foo<T> where SiameseCat:T) e que não há possibilidade de ter um tipo genérico que não seja convertível Object. IMHO, .NET se beneficiaria de tipos agregados que eram semelhantes a estruturas, mas ainda mais despojados. Se KeyValuePair<TKey,TValue>fosse desse tipo, um IEnumerable<KeyValuePair<SiameseCat,FordFocus>>poderia ser convertido para IEnumerable<KeyValuePair<Animal,Vehicle>>, mas apenas se o tipo não pudesse ser encaixotado.
Supercat

Respostas:


24

Enquanto os genéricos são dominantes na comunidade de programação funcional há décadas, a adição de genéricos a linguagens de programação orientadas a objetos oferece alguns desafios exclusivos, especificamente a interação de subtipagem e genéricos.

No entanto, mesmo se focarmos nas linguagens de programação orientada a objetos e no Java em particular, um sistema genérico muito melhor poderia ter sido projetado:

  1. Tipos genéricos devem ser admissíveis onde quer que estejam. Em particular, se Tfor um parâmetro de tipo, as seguintes expressões deverão ser compiladas sem avisos:

    object instanceof T; 
    T t = (T) object;
    T[] array = new T[1];
    

    Sim, isso exige que os genéricos sejam reificados, assim como todos os outros tipos no idioma.

  2. A covariância e a contravariância de um tipo genérico devem ser especificadas em (ou deduzidas de) sua declaração, em vez de toda vez que o tipo genérico é usado, para que possamos escrever

    Future<Provider<Integer>> s;
    Future<Provider<Number>> o = s; 
    

    ao invés de

    Future<? extends Provider<Integer>> s;
    Future<? extends Provider<? extends Number>> o = s;
    
  3. Como os tipos genéricos podem demorar um pouco, não precisamos especificá-los de forma redundante. Ou seja, devemos ser capazes de escrever

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (var e : map.values()) {
        for (var list : e.values()) {
            for (var person : list) {
                greet(person);
            }
        }
    }
    

    ao invés de

    Map<String, Map<String, List<LanguageDesigner>>> map;
    for (Map<String, List<LanguageDesigner>> e : map.values()) {
        for (List<LanguageDesigner> list : e.values()) {
            for (LanguageDesigner person : list) {
                greet(person);
            }
        }
    }
    
  4. Qualquer tipo deve ser admissível como parâmetro de tipo, não apenas tipos de referência. (Se podemos ter um int[], por que não podemos ter um List<int>)?

Tudo isso é possível em C #.


1
Isso também eliminaria os genéricos autorreferenciais? E se eu quiser dizer que um objeto comparável pode se comparar a qualquer coisa do mesmo tipo ou subclasse? Isso pode ser feito? Ou, se eu escrever um método de classificação que aceite listas com objetos comparáveis, todos precisam ser comparáveis ​​entre si. Enum é outro bom exemplo: Enum <E estende Enum <E>>. Não estou dizendo que um sistema de tipos deve ser capaz de fazer isso; estou curioso para saber como o C # lida com essas situações.
Peter

1
A inferência de tipo genérica do Java 7 e a ajuda automática do C ++ ajudam com algumas dessas preocupações, mas são açúcar sintático e não alteram os mecanismos subjacentes.

@Snowman A inferência de tipo do Java tem alguns casos de canto realmente desagradáveis, como não trabalhar com classes anônimas e não encontrar os limites certos para caracteres curinga quando você avalia um método genérico como argumento para outro método genérico.
Doval

@Doval é por isso que eu disse que ajuda com algumas das preocupações: não corrige nada e não resolve tudo. Os genéricos Java têm muitos problemas: embora melhores que os tipos brutos, eles certamente causam muitas dores de cabeça.

34

O uso de subtipos cria muitas complicações ao executar a programação genérica. Se você insistir em usar uma linguagem com subtipos, precisará aceitar que há uma certa complexidade inerente à programação genérica que a acompanha. Alguns idiomas fazem isso melhor que outros, mas você só pode levar isso até agora.

Compare isso com os genéricos de Haskell, por exemplo. Eles são simples o suficiente para que, se você usar a inferência de tipo, possa escrever uma função genérica correta por acidente . Na verdade, se você especificar um único tipo, o compilador muitas vezes diz para si mesmo: "Bem, eu estava indo fazer este genérico, mas você me pediu para fazê-lo apenas para ints, então que seja."

É certo que as pessoas usam o sistema de tipos de Haskell de maneiras surpreendentemente complexas, tornando-o o banimento de qualquer novato, mas o sistema de tipos subjacente em si é elegante e muito admirado.


1
Obrigado por esta resposta. Este artigo começa com alguns exemplos de Joshua Bloch de onde os genéricos se complicam demais: artima.com/weblogs/viewpost.jsp?thread=222021 . Existe uma diferença na cultura entre Java e Haskell, onde tais construções seriam consideradas boas em Haskell, ou existe uma diferença real no sistema de tipos de Haskell que evita tais situações?
Peter

10
@ Peter Haskell não possui subtipo e, como Karl disse, o compilador pode inferir os tipos automaticamente, incluindo restrições como "tipo adeve ser algum tipo de número inteiro".
Doval

Em outras palavras , covariância , em idiomas como o Scala.
Paul Draper

14

Houve muita pesquisa sobre a combinação de genéricos com subtipagem ocorrida há cerca de 20 anos. A linguagem de programação Thor desenvolvida pelo grupo de pesquisa de Barbara Liskov no MIT tinha uma noção de "onde" cláusulas que permitem especificar os requisitos do tipo que você está parametrizando. (Isso é semelhante ao que o C ++ está tentando fazer com os Conceitos .)

O artigo que descreve os genéricos de Thor e como eles interagem com os subtipos de Thor é: Dia, M; Gruber, R; Liskov, B; Myers, AC: subtipos vs. cláusulas where: constrangimento do polimorfismo paramétrico , ACM Conf em Obj-Oriented Prog, Sys, Lang e Apps , (OOPSLA-10): 156-158, 1995.

Eu acredito que eles, por sua vez, se basearam no trabalho que foi feito no Emerald no final dos anos 80. (Não li esse trabalho, mas a referência é: Black, A; Hutchinson, N; Jul, E; Levy, H; Carter, L: Distribuição e tipos abstratos em Emerald , _IEEE T. Software Eng., 13 ( 1): 65-76, 1987.

Tanto Thor quanto Emerald eram "linguagens acadêmicas", então provavelmente não tiveram uso suficiente para as pessoas realmente entenderem se as cláusulas where (conceitos) realmente resolvem quaisquer problemas reais. É interessante ler o artigo de Bjarne Stroustrup sobre por que a primeira tentativa de Concepts em C ++ falhou: Stroustrup, B: A decisão "Remover conceitos" do C ++ 0x , Dr. Dobbs , 22 de julho de 2009. (Mais informações na página inicial do Stroustrup . )

Outra direção que as pessoas parecem estar tentando é algo chamado traços . Por exemplo, a linguagem de programação Rust da Mozilla usa características. Pelo que entendi (o que pode estar completamente errado), declarar que uma classe satisfaz uma característica é como dizer que uma classe implementa uma interface, mas você está dizendo "se comporta como um" em vez de "é um". Parece que as novas linguagens de programação Swift da Apple estão usando um conceito semelhante de protocolos para especificar restrições nos parâmetros dos genéricos .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.