Qual é a vantagem da inferência de tipo?

29

Parece que todas as novas linguagens de programação ou pelo menos as que se tornaram populares usam inferência de tipos. Até o Javascript obteve tipos e inferência de tipos através de várias implementações (Acscript, texto datilografado etc). Parece ótimo para mim, mas estou me perguntando se há algum trade-offs ou por que, digamos, Java ou as boas linguagens antigas não têm inferência de tipo

  • Ao declarar uma variável em Go sem especificar seu tipo (usando var sem um tipo ou a sintaxe: =), o tipo da variável é deduzido do valor no lado direito.
  • D permite escrever grandes fragmentos de código sem especificar tipos redundantes, como fazem as linguagens dinâmicas. Por outro lado, a inferência estática deduz tipos e outras propriedades de código, fornecendo o melhor dos mundos estático e dinâmico.
  • O mecanismo de inferência de tipo no Rust é bastante inteligente. Ele faz mais do que olhar para o tipo de valor r durante uma inicialização. Também mostra como a variável é usada posteriormente para inferir seu tipo.
  • O Swift usa a inferência de tipo para calcular o tipo apropriado. A inferência de tipo permite que um compilador deduza o tipo de uma expressão específica automaticamente quando compila seu código, simplesmente examinando os valores que você fornece.
O usuário sem chapéu
fonte
3
No C #, as diretrizes gerais dizem que nem sempre é usado varporque às vezes pode prejudicar a legibilidade.
Mephy
5
"... ou por que digamos que Java ou as boas linguagens antigas não têm inferência de tipo" As razões são provavelmente históricas; O ML apareceu 1 ano após C, de acordo com a Wikipedia e tinha inferência de tipo. Java estava tentando atrair desenvolvedores de C ++ . O C ++ começou como uma extensão do C, e as principais preocupações de C eram ser um invólucro portátil sobre a montagem e ser fácil de compilar. Pelo que valha a pena, li que a subtipagem torna a inferência de tipo indecidível no caso geral também.
D
3
@Doval Scala parece fazer um bom trabalho ao inferir tipos, para um idioma que suporta herança de subtipo. Não é tão bom quanto qualquer um dos idiomas da família ML, mas provavelmente é o melhor que você poderia pedir, dado o design do idioma.
KChaloux
12
Vale a pena fazer uma distinção entre dedução de tipo (monodirecional, como C # vare C ++ auto) e inferência de tipo (bidirecional, como Haskell let). No primeiro caso, o tipo de um nome pode ser deduzido apenas do inicializador - seus usos devem seguir o tipo do nome. No último caso, o tipo de um nome também pode ser deduzido de seus usos - o que é útil porque você pode escrever simplesmente []para uma sequência vazia, independentemente do tipo de elemento, ou newEmptyMVarpara uma nova referência mutável nula, independentemente do referente tipo.
Jon Purdy
A inferência de tipo pode ser incrivelmente complicada de implementar, especialmente com eficiência. As pessoas não querem que sua complexidade de compilação sofra uma explosão exponencial de expressões mais complexas. Leitura interessante: cocoawithlove.com/blog/2016/07/12/type-checker-issues.html
Alexander - Reinstate Monica

Respostas:

43

O sistema de tipos de Haskell é totalmente inferível (deixando de lado a recursão polimórfica, certas extensões de linguagem e a temida restrição de monomorfismo ), mas os programadores ainda frequentemente fornecem anotações de tipo no código-fonte, mesmo quando não precisam. Por quê?

  1. As anotações de tipo servem como documentação . Isto é especialmente verdade com tipos tão expressivos quanto os de Haskell. Dado o nome de uma função e seu tipo, geralmente você pode adivinhar o que a função faz, especialmente quando a função é parametricamente polimórfica.
  2. As anotações de tipo podem impulsionar o desenvolvimento . Escrever uma assinatura de tipo antes de escrever o corpo de uma função é como um desenvolvimento orientado a teste. Na minha experiência, uma vez que você compila uma função Haskell, ela geralmente funciona na primeira vez. (Obviamente, isso não impede a necessidade de testes automatizados!)
  3. Tipos explícitos podem ajudá-lo a verificar suas suposições . Quando estou tentando entender algum código que já funciona, frequentemente o uso com o que acredito serem as anotações de tipo corretas. Se o código ainda compilar, sei que o compreendi. Caso contrário, li a mensagem de erro.
  4. As assinaturas de tipo permitem especializar funções polimórficas . Muito ocasionalmente, uma API é mais expressiva ou útil se determinadas funções não forem polimórficas. O compilador não irá reclamar se você der a uma função um tipo menos geral do que seria inferido. O exemplo clássico é map :: (a -> b) -> [a] -> [b]. Sua forma mais geral ( fmap :: Functor f => (a -> b) -> f a -> f b) se aplica a todos os Functors, não apenas às listas. Mas considerou-se que mapseria mais fácil de entender para iniciantes, por isso vive ao lado de seu irmão maior.

No geral, as desvantagens de um sistema estaticamente digitado, mas inferível, são as mesmas que as desvantagens da digitação estática em geral, uma discussão bastante desgastada neste site e em outras (Googling "desvantagens de digitação estática", você terá centenas de páginas de guerras de chamas). Obviamente, algumas dessas desvantagens são melhoradas pela menor quantidade de anotações de tipo em um sistema inferível. Além disso, a inferência de tipo tem suas próprias vantagens: o desenvolvimento acionado por orifício não seria possível sem a inferência de tipo.

O Java * prova que uma linguagem que exige muitas anotações de tipo fica irritante, mas com muito poucas você perde as vantagens que descrevi acima. Os idiomas com inferência do tipo opt-out atingem um equilíbrio agradável entre os dois extremos.

* Mesmo o Java, esse grande bode expiatório, realiza uma certa quantidade de inferência de tipo local . Em uma declaração como Map<String, Integer> = new HashMap<>();, você não precisa especificar o tipo genérico do construtor. Por outro lado, as linguagens no estilo ML são tipicamente globalmente inferíveis.

Benjamin Hodgson
fonte
2
Mas você não é iniciante;) Você precisa entender as classes e tipos de tipos antes de realmente "entender" Functor. Lista e mapé mais provável que seja familiar a Haskellers não experientes.
Benjamin Hodgson
1
Você consideraria os C # varum exemplo de inferência de tipo?
Benjamin Hodgson
1
Sim, c # varé um exemplo adequado.
D
1
O site já está sinalizando esta discussão como muito longa, então essa será minha última resposta. No primeiro, o compilador deve determinar o tipo de x; neste último, não há um tipo a determinar, todos os tipos são conhecidos e você apenas precisa verificar se a expressão faz sentido. A diferença se torna mais importante à medida que você passa por exemplos triviais e xé usada em vários lugares; o compilador deve então verificar os locais onde xé usado para determinar 1) é possível atribuir xum tipo para que o código digite check? 2) Se for, qual é o tipo mais geral que podemos atribuir?
Doval
1
Observe que a new HashMap<>();sintaxe foi adicionada apenas no Java 7, e as lambdas no Java 8 permitem bastante inferência de tipo "real".
Michael Borgwardt
8

Em C #, a inferência de tipo ocorre no tempo de compilação, portanto, o custo de tempo de execução é zero.

Por uma questão de estilo, varé usado para situações em que é inconveniente ou desnecessário especificar manualmente o tipo. Linq é uma dessas situações. Outro é:

var s = new SomeReallyLongTypeNameWith<Several, Type, Parameters>(andFormal, parameters);

sem o qual você repetiria seu nome de tipo realmente longo (e os parâmetros de tipo) em vez de simplesmente dizer var.

Use o nome real do tipo ao ser explícito para melhorar a clareza do código.

Existem algumas situações em que a inferência de tipo não pode ser usada, como declarações de variáveis ​​de membros cujos valores são definidos no momento da construção ou onde você realmente deseja que o intellisense funcione corretamente (o IDE de Hackerrank não fará o sentido de inteligência dos membros da variável, a menos que você declare o tipo explicitamente) .

Robert Harvey
fonte
16
"No C #, a inferência de tipo ocorre no tempo de compilação, portanto, o custo do tempo de execução é zero." A inferência de tipo ocorre no momento da compilação, por definição, então é o caso em todos os idiomas.
D
Muito melhor.
Robert Harvey
1
@Doval é possível executar inferência de tipo durante a compilação JIT, que claramente tem um custo de tempo de execução.
Jules
6
@Jules Se você chegou ao ponto de executar o programa, ele já foi verificado; não há mais nada a inferir. O que você está falando não costuma ser chamado de inferência de tipo, embora não tenha certeza de qual seja o termo adequado.
D
7

Boa pergunta!

  1. Como o tipo não é explicitamente anotado, às vezes pode dificultar a leitura do código - levando a mais erros. Corretamente usado que, obviamente, torna o código mais limpo e mais legível. Se você é pessimista e pensa que a maioria dos programadores é ruim (ou trabalha onde a maioria dos programadores é ruim), isso será uma perda líquida.
  2. Embora o algoritmo de inferência de tipo seja relativamente simples, ele não é gratuito . Esse tipo de coisa aumenta um pouco o tempo de compilação.
  3. Como o tipo não é explicitamente anotado, seu IDE também não consegue adivinhar o que você está tentando fazer, prejudicando o preenchimento automático e auxiliares similares durante o processo de declaração.
  4. Combinado com sobrecargas de função, você pode ocasionalmente entrar em situações em que o algoritmo de inferência de tipo não pode decidir qual caminho seguir, levando a anotações mais feias do estilo de conversão. (Isso acontece bastante com a sintaxe da função anônima do C #, por exemplo).

E há mais linguagens esotéricas que não podem fazer suas estranhezas sem anotação explícita de tipo. Até agora, não há nenhum que eu saiba que seja comum / popular / promissor o suficiente para mencionar, exceto de passagem.

Telastyn
fonte
1
o preenchimento automático não dá a mínima para a inferência de tipo. Não importa como o tipo foi decidido, apenas o que o tipo possui. E os problemas com as lambdas do C # não têm nada a ver com inferência, são porque as lambdas são polimórficas, mas o sistema de tipos não pode lidar com isso - o que não tem nada a ver com inferência.
DeadMG
2
@deadmg - claro, uma vez que a variável é declarada, não há problema, mas enquanto você digita a declaração da variável, ela não pode restringir as opções do lado direito ao tipo declarado, pois não há um tipo declarado.
Telastyn
@deadmg - quanto aos lambdas, não sou muito claro sobre o que você quer dizer, mas tenho certeza de que não está totalmente correto com base no meu entendimento de como as coisas funcionam. Mesmo algo tão simples como var foo = x => x;falha porque a linguagem precisa inferir xaqui e não tem nada para continuar. Quando as lambdas são construídas, elas são construídas quando delegados de tipo explícito Func<int, int> foo = x => xsão construídos no CIL, como Func<int, int> foo = new Func<int, int>(x=>x);onde o lambda é construído como uma função gerada e de tipo explícito. Para mim, inferir o tipo de xé parte integrante de inferência de tipos ...
Telastyn
3
@Telastyn Não é verdade que o idioma não tenha nada para continuar. Todas as informações necessárias para digitar a expressão x => xestão contidas no próprio lambda - não se referem a nenhuma variável do escopo circundante. Qualquer linguagem funcional sã iria correctamente inferir que seu tipo é "para todos os tipos a, a -> a". Eu acho que o DeadMG está tentando dizer é que o sistema de tipos do C # carece da propriedade type principal, o que significa que sempre é possível descobrir o tipo mais geral para qualquer expressão. É uma propriedade muito fácil de destruir.
Doval
@Doval - enh ... não existem objetos de função genérica em C #, que eu acho que são sutilmente diferentes do que vocês estão falando, mesmo que isso leve ao mesmo problema.
Telastyn
4

Parece ótimo para mim, mas estou me perguntando se há algum trade-offs ou por que, digamos, Java ou as boas linguagens antigas não têm inferência de tipo

O Java passa a ser a exceção e não a regra aqui. Até o C ++ (que eu acredito qualificar como uma "boa e antiga linguagem" :)) suporta inferência de tipo com a autopalavra - chave desde o padrão C ++ 11. Ele não funciona apenas com declaração de variável, mas também como tipo de retorno de função, o que é especialmente útil em algumas funções complexas de modelo.

Digitação implícita e inferência de tipo têm muitos casos de uso bons e também existem alguns casos de uso em que você realmente não deve fazê-lo. Às vezes, isso é questão de gosto e também assunto de debate.

Mas, sem dúvida, há casos de uso bons, é por si só uma razão legítima para qualquer linguagem implementá-lo. Também não é um recurso difícil de implementar, sem penalidade no tempo de execução e não afeta o tempo de compilação significativamente.

Não vejo uma desvantagem real em dar uma oportunidade ao desenvolvedor de usar inferência de tipo.

Algumas pessoas que responderam dizem que a digitação explícita é boa às vezes e certamente estão certas. Mas não oferecer suporte à digitação implícita significaria que um idioma impõe a digitação explícita o tempo todo.

Portanto, a verdadeira desvantagem é quando uma linguagem não suporta digitação implícita, porque com isso afirma que não há razão legítima para o desenvolvedor usá-la, o que não é verdade.

Gábor Angyal
fonte
1

A principal distinção entre um sistema de inferência do tipo Hindley-Milner e inferência do tipo Go é a direção do fluxo de informações. No HM, as informações de tipo fluem para frente e para trás via unificação; no Go, as informações de tipo fluem apenas para frente: calcula apenas substituições para frente.

A inferência de tipo HM é uma inovação esplêndida que funciona bem com linguagens funcionais de tipo polimórfico, mas os autores de Go provavelmente argumentam que ela tenta fazer muito:

  1. O fato de a informação fluir para frente e para trás significa que a inferência do tipo HM é um assunto muito não-local; na ausência de anotações de tipo, todas as linhas de código do seu programa podem estar contribuindo para a digitação de uma única linha de código. Se você tiver apenas substituição direta, você sabe que a fonte de um erro de tipo deve estar no código que precede seu erro; não o código que vem depois.

  2. Com a inferência do tipo HM, você tende a pensar em restrições: ao usar um tipo, você restringe os possíveis tipos que ele pode ter. No final do dia, pode haver algumas variáveis ​​de tipo que são deixadas totalmente sem restrições. O insight da inferência de tipo HM é que, esses tipos realmente não importam e, portanto, são transformados em variáveis ​​polimórficas. No entanto, esse polimorfismo extra pode ser indesejável por várias razões. Primeiro, como algumas pessoas apontaram, esse polimorfismo extra pode ser indesejável: HM concluindo um tipo falso e polimórfico para algum código falso e levar a erros estranhos mais tarde. Segundo, quando um tipo é deixado polimórfico, isso pode ter consequências para o comportamento em tempo de execução. Por exemplo, um resultado intermediário excessivamente polimórfico é a razão pela qual 'mostra. read 'é considerado ambíguo em Haskell; como outro exemplo,

Edward Z. Yang
fonte
2
Infelizmente, isso parece uma boa resposta para uma pergunta diferente. O OP está perguntando sobre "inferência de tipo no estilo Go" versus idiomas que não têm nenhuma inferência de tipo, não "estilo no estilo Go" versus HM.
Ixrec 22/03/16
-2

Dói a legibilidade.

Comparar

var stuff = someComplexFunction()

vs

List<BusinessObject> stuff = someComplexFunction()

É especialmente um problema ao ler fora de um IDE, como no github.

miguel
fonte
4
este não parece oferecer nada substancial sobre pontos feitos e explicado em anteriores 6 respostas
mosquito
Se você usar o nome próprio em vez de "complexo ilegível", varpoderá ser usado sem prejudicar a legibilidade. var objects = GetBusinessObjects();
Fabio
Ok, mas você está vazando o sistema de tipos para os nomes de variáveis. É como uma notação húngara. Após as refatorações, é mais provável que você acabe com nomes que não correspondem ao código. Em geral, o estilo funcional leva você a perder as vantagens de namespacing natural que as classes fornecem. Então você acaba com mais squareArea () em vez de square.area ().
miguel