Por que a inferência de tipo é útil?

37

Eu leio código com mais freqüência do que escrevo código e estou assumindo que a maioria dos programadores que trabalham em software industrial faz isso. A vantagem da inferência de tipo que assumo é menos verbosidade e menos código escrito. Mas, por outro lado, se você ler código com mais frequência, provavelmente desejará código legível.

O compilador infere o tipo; existem algoritmos antigos para isso. Mas a verdadeira questão é por que eu, o programador, gostaria de inferir o tipo de minhas variáveis ​​quando leio o código? Não é mais rápido alguém ler o tipo do que pensar que tipo existe?

Edit: Como conclusão, eu entendo por que é útil. Mas na categoria de recursos de linguagem, eu o vejo em um balde com sobrecarga de operador - útil em alguns casos, mas afetando a legibilidade se for abusada.

m3th0dman
fonte
5
Na minha experiência, os tipos são muito mais importantes ao escrever código do que lê-lo. Ao ler o código, estou procurando algoritmos e blocos específicos aos quais variáveis ​​bem nomeadas geralmente me direcionam. Eu realmente não preciso digitar o código de verificação apenas para ler e entender o que está fazendo, a menos que esteja muito mal escrito. No entanto, ao ler um código cheio de detalhes desnecessários que não estou procurando (como muitas anotações de tipo), muitas vezes fica mais difícil encontrar os bits que estou procurando. A inferência de tipo que eu diria é um grande benefício para ler muito mais do que escrever código.
Jimmy Hoffa
Depois de encontrar o trecho de código que estou procurando, posso começar a digitá-lo, mas a qualquer momento você não deve se concentrar em mais do que talvez 10 linhas de código; nesse momento, não há problemas em fazer o inferência você mesmo, porque você está separando mentalmente todo o bloco e provavelmente usando ferramentas para ajudá-lo a fazê-lo de qualquer maneira. Descobrir os tipos das 10 linhas de código em que você está tentando zonear raramente leva muito tempo, mas essa é a parte em que você mudou da leitura para a escrita de qualquer maneira, o que é muito menos comum.
Jimmy Hoffa
Lembre-se de que, mesmo que um programador leia código com mais frequência do que escreve, isso não significa que um pedaço de código seja lido com mais frequência do que escrito. Muitos códigos podem ter vida curta ou nunca serem lidos novamente, e pode não ser fácil saber qual código sobreviverá e deve ser gravado com a máxima legibilidade.
jpa
2
Ao elaborar o primeiro ponto de @JimmyHoffa, considere a leitura em geral. Uma frase é mais fácil de analisar e ler, e muito menos entender, quando se concentra na parte do discurso de suas palavras individuais? "A vaca (artigo) (substantivo-singular) pulou (verbo-passado) sobre (preposição) a lua (artigo) (substantivo). (Período de pontuação)".
Zev Spitz

Respostas:

46

Vamos dar uma olhada no Java. Java não pode ter variáveis ​​com tipos inferidos. Isso significa que frequentemente tenho que especificar o tipo, mesmo que seja perfeitamente óbvio para um leitor humano qual é o tipo:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

E, às vezes, é simplesmente irritante soletrar todo o tipo.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

Essa digitação estática detalhada fica no meu caminho, o programador. A maioria das anotações de tipo são repetições repetitivas e sem conteúdo do que já sabemos. No entanto, eu gosto da digitação estática, pois ela pode realmente ajudar na descoberta de bugs, portanto, a digitação dinâmica nem sempre é uma boa resposta. A inferência de tipo é o melhor dos dois mundos: posso omitir os tipos irrelevantes, mas ainda assim, certifique-se de que meu programa (tipo-) faça check-out.

Embora a inferência de tipo seja realmente útil para variáveis ​​locais, ela não deve ser usada para APIs públicas que precisam ser documentadas sem ambiguidade. E, às vezes, os tipos são realmente críticos para entender o que está acontecendo no código. Nesses casos, seria tolice confiar apenas na inferência de tipo.

Existem muitos idiomas que suportam inferência de tipo. Por exemplo:

  • C ++. A autopalavra-chave aciona a inferência de tipo. Sem ele, escrever os tipos de lambdas ou entradas em contêineres seria um inferno.

  • C #. Você pode declarar variáveis ​​com var, o que aciona uma forma limitada de inferência de tipo. Ele ainda gerencia a maioria dos casos em que você deseja inferência de tipo. Em certos lugares, você pode deixar de fora o tipo completamente (por exemplo, em lambdas).

  • Haskell e qualquer idioma da família ML. Embora o sabor específico da inferência de tipo usado aqui seja bastante poderoso, você ainda vê anotações de tipo para funções e por dois motivos: O primeiro é a documentação e o segundo é uma verificação de que a inferência de tipo realmente encontrou os tipos que você esperava. Se houver uma discrepância, provavelmente haverá algum tipo de bug.

amon
fonte
13
Observe também que o C # possui tipos anônimos, ou seja, tipos sem nome, mas o C # possui um sistema de tipos nominais, ou seja, um sistema de tipos com base em nomes. Sem inferência de tipo, esses tipos nunca poderiam ser usados!
Jörg W Mittag 28/09
10
Alguns exemplos são um pouco artificial, na minha opinião. A inicialização para 42 não significa automaticamente que a variável é um int, pode ser qualquer tipo numérico, incluindo par char. Também não vejo por que você gostaria de especificar o tipo inteiro para Entryquando você pode apenas digitar o nome da classe e deixar seu IDE fazer a importação necessária. O único caso em que você precisa especificar o nome inteiro é quando você tem uma classe com o mesmo nome em seu próprio pacote. Mas parece-me um design ruim de qualquer maneira.
Malcolm
10
@ Malcolm Sim, todos os meus exemplos são inventados. Eles servem para ilustrar um ponto. Quando escrevi o intexemplo, estava pensando no (na minha opinião, um comportamento bastante sensato) da maioria dos idiomas que apresentam inferência de tipo. Eles geralmente inferem um intou Integerou como é chamado nesse idioma. A beleza da inferência de tipo é que é sempre opcional; você ainda pode especificar um tipo diferente se precisar de um. Quanto ao Entryexemplo: bom argumento, vou substituí-lo por Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. O Java nem sequer tem aliases de tipo :(
amon
4
@ m3th0dman Se o tipo é importante para a compreensão, você ainda pode mencioná-lo explicitamente. A inferência de tipo é sempre opcional. Mas aqui, o tipo de colKeyé óbvio e irrelevante: nós apenas nos preocupamos em ser adequado como o segundo argumento de doSomethingWith. Se eu extraísse esse loop em uma função que produza um iterável de (key1, key2, value)-triples, a assinatura mais geral seria <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Dentro dessa função, o tipo real de colKey( Integer, not K2) é absolutamente irrelevante.
amon
4
@ m3th0dman é uma declaração bastante abrangente, sobre o código " mais " ser esse ou aquilo. Estatísticas anedóticas. Não há certamente nenhum ponto em escrever o tipo duas vezes em initializers: View.OnClickListener listener = new View.OnClickListener(). Você ainda saberia o tipo, mesmo que o programador fosse "preguiçoso" e o reduzisse para var listener = new View.OnClickListener(se isso fosse possível). Este tipo de redundância é comum - Não vou arriscar um palpite aqui - e removê-lo não resultam de pensar em futuros leitores. Todo recurso de idioma deve ser usado com cuidado, não estou questionando isso.
21913 Konrad Morawski
26

É verdade que o código é lido com muito mais frequência do que está escrito. No entanto, a leitura também leva tempo, e duas telas de código são mais difíceis de navegar e ler do que uma tela de código; portanto, precisamos priorizar para compactar a melhor proporção de informações úteis / esforço de leitura. Este é um princípio geral de UX: Muita informação sobrecarrega e degrada a eficácia da interface.

E é minha experiência que , muitas vezes , o tipo exato não é (isso) importante. Certamente você às vezes expressões ninho: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Cada uma delas contém subexpressões avaliadas para um valor cujo tipo não está gravado. No entanto, eles são perfeitamente claros. Estamos bem em deixar o tipo não declarado, porque é fácil descobrir o contexto e escrevê-lo faria mais mal do que bem (confundir a compreensão do fluxo de dados, ocupar tela valiosa e espaço de memória de curto prazo).

Uma razão para não aninhar expressões como se não houvesse amanhã é que as linhas ficam longas e o fluxo de valores se torna incerto. A introdução de uma variável temporária ajuda nisso, impõe uma ordem e atribui um nome a um resultado parcial. No entanto, nem tudo o que se beneficia com esses aspectos também se beneficia de ter seu tipo explicitado:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

Importa se useré um objeto de entidade, um número inteiro, uma string ou outra coisa? Para a maioria dos propósitos, não basta, basta saber que representa um usuário, vem da solicitação HTTP e é usado para buscar o nome a ser exibido no canto inferior direito da resposta.

E quando se faz a matéria, o autor é livre para escrever o tipo. Essa é uma liberdade que deve ser usada com responsabilidade, mas o mesmo vale para tudo o que pode melhorar a legibilidade (nomes de variáveis ​​e funções, formatação, design da API, espaço em branco). E, de fato, a convenção em Haskell e ML (onde tudo pode ser inferido sem esforço extra) é escrever os tipos de funções de funções não locais e também de variáveis ​​e funções locais sempre que apropriado. Somente iniciantes permitem inferir todo tipo.


fonte
2
+1 Esta deve ser a resposta aceita. Isso vai direto ao ponto de por que a inferência de tipo é uma ótima idéia.
Christian Hayter
O tipo exato de userimporta se você está tentando estender a função, porque determina o que você pode fazer com o user. Isso é importante se você deseja adicionar alguma verificação de integridade (devido a uma vulnerabilidade de segurança, por exemplo) ou se esqueceu que realmente precisa fazer algo com o usuário, além de apenas exibi-lo. É verdade que esses tipos de leitura para expansão são menos frequentes do que apenas ler o código, mas também são uma parte vital do nosso trabalho.
precisa saber é
@cmaster E você sempre pode descobrir esse tipo com bastante facilidade (a maioria dos IDEs dirá a você, e há a solução de baixa tecnologia de causar intencionalmente um erro de tipo e deixar o compilador imprimir o tipo real), está apenas fora do caminho, portanto não incomoda você no caso comum.
4

Eu acho que a inferência de tipo é muito importante e deve ser suportada em qualquer linguagem moderna. Todos nós desenvolvemos IDEs e eles podem ajudar muito, caso você queira saber o tipo inferido, apenas alguns de nós invadimos vi. Pense em verbosidade e código de cerimônia em Java, por exemplo.

  Map<String,HashMap<String,String>> map = getMap();

Mas você pode dizer que está tudo bem, meu IDE vai me ajudar, pode ser um ponto válido. No entanto, alguns recursos não estariam lá sem a ajuda da inferência de tipo, tipos anônimos C #, por exemplo.

 var person = new {Name="John Smith", Age = 105};

O Linq não seria tão bom como é agora sem a ajuda da inferência de tipo, Selectpor exemplo

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

Esse tipo anônimo será inferido ordenadamente na variável.

Não gosto de inferência de tipo em tipos de retorno, Scalaporque acho que seu ponto se aplica aqui; deve ficar claro para nós o que uma função retorna para que possamos usar a API com mais fluência

Sleiman Jneidi
fonte
Map<String,HashMap<String,String>>? Certamente, se você não estiver usando tipos, soletrá-los traz pouco benefício. Table<User, File, String>é mais informativo e há vantagens em escrevê-lo.
MikeFHay
4

Eu acho que a resposta para isso é realmente simples: economiza a leitura e a gravação de informações redundantes. Especialmente nas linguagens orientadas a objetos, nas quais você tem um tipo nos dois lados do sinal de igual.

O que também informa quando você deve ou não usá-lo - quando as informações não são redundantes.

jmoreno
fonte
3
Bem, tecnicamente, as informações são sempre redundantes quando é possível omitir assinaturas manuais: caso contrário, o compilador não seria capaz de inferi-las! Mas entendi o que você quer dizer: quando você apenas duplica uma assinatura em vários pontos em uma única exibição, é realmente redundante para o cérebro , enquanto alguns tipos bem posicionados fornecem informações que você precisaria pesquisar por um longo tempo, possivelmente com um boas transformações não óbvias.
usar o seguinte comando
@leftaroundabout: redundante quando lido pelo programador.
precisa saber é o seguinte
3

Suponha que alguém veja o código:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Se someBigLongGenericTypeé atribuível a partir do tipo de retorno someFactoryMethod, qual a probabilidade de alguém ler o código perceber se os tipos não correspondem exatamente e com que facilidade alguém que notou a discrepância pode reconhecer se foi intencional ou não?

Ao permitir a inferência, um idioma pode sugerir a alguém que está lendo o código que, quando o tipo de uma variável é declarado explicitamente, a pessoa deve tentar encontrar um motivo para isso. Isso, por sua vez, permite que as pessoas que estão lendo o código concentrem melhor seus esforços. Se, por outro lado, na grande maioria das vezes em que um tipo é especificado, ele é exatamente o mesmo que seria inferido, alguém que está lendo um código pode ser menos propenso a perceber os momentos em que é sutilmente diferente .

supercat
fonte
2

Vejo que já existem várias respostas excelentes. Algumas das quais repetirei, mas às vezes você só quer colocar as coisas com suas próprias palavras. Vou comentar com alguns exemplos do C ++, porque essa é a linguagem com a qual tenho mais familiaridade.

O que é necessário nunca é imprudente. A inferência de tipo é necessária para tornar outros recursos de linguagem práticos. Em C ++, é possível ter tipos indizíveis.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

O C ++ 11 adicionou lambdas que também são indizíveis.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

A inferência de tipo também sustenta modelos.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

Mas suas perguntas foram "por que eu, o programador, gostaria de inferir o tipo de minhas variáveis ​​quando leio o código? Não é mais rápido para alguém apenas ler o tipo do que pensar que tipo existe?"

A inferência de tipo remove a redundância. Quando se trata de ler código, às vezes pode ser mais rápido e fácil ter informações redundantes no código, mas a redundância pode ofuscar as informações úteis . Por exemplo:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Não é preciso muita familiaridade com a biblioteca padrão para um programador de C ++ identificar que eu sou um iterador, i = v.begin()portanto a declaração de tipo explícita é de valor limitado. Por sua presença, oculta os detalhes mais importantes (como os que iapontam para o início do vetor). A boa resposta de @amon fornece um exemplo ainda melhor de verbosidade que oculta detalhes importantes. Em contraste, o uso de inferência de tipo dá maior destaque aos detalhes importantes.

std::vector<int> v;
auto i = v.begin();

Embora a leitura do código seja importante, não é suficiente, em algum momento você precisará parar de ler e começar a escrever um novo código. A redundância no código torna a modificação do código mais lenta e mais difícil. Por exemplo, digamos que tenho o seguinte fragmento de código:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

No caso em que eu preciso alterar o tipo de valor do vetor para alterar o código para:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

Nesse caso, tenho que modificar o código em dois lugares. Contraste com a inferência de tipo, onde o código original é:

std::vector<int> v;
auto i = v.begin();

E o código modificado:

std::vector<double> v;
auto i = v.begin();

Note que agora só preciso alterar uma linha de código. Extrapole isso para um programa grande e a inferência de tipo pode propagar as alterações para os tipos muito mais rapidamente do que com um editor.

A redundância no código cria a possibilidade de erros. Sempre que seu código depende de duas informações mantidas equivalentes, existe a possibilidade de erro. Por exemplo, há uma inconsistência entre os dois tipos nesta declaração que provavelmente não se destina:

int pi = 3.14159;

A redundância torna a intenção mais difícil de discernir. Em alguns casos, a inferência de tipo pode ser mais fácil de ler e entender, porque é mais simples que a especificação de tipo explícita. Considere o fragmento de código:

int y = sq(x);

No caso que sq(x)retorna um int, não é óbvio se yé um intporque é o tipo de retorno sq(x)ou porque é adequado às instruções que são usadas y. Se eu alterar outro código para que sq(x)não retorne mais int, é incerto a partir dessa linha apenas se o tipo de ydeve ser atualizado. Contraste com o mesmo código, mas usando inferência de tipo:

auto y = sq(x);

Neste, a intenção é clara, ydeve ser do mesmo tipo retornada por sq(x). Quando o código altera o tipo de retorno sq(x), o tipo de yalteração é correspondido automaticamente.

Em C ++, existe uma segunda razão pela qual o exemplo acima é mais simples com inferência de tipo, a inferência de tipo não pode introduzir conversão implícita de tipo. Se o tipo de retorno de sq(x)não for int, o compilador insere silenciosamente uma conversão implícita em int. Se o tipo de retorno de sq(x)é um tipo complexo de tipo que define operator int(), essa chamada de função oculta pode ser arbitrariamente complexa.

Bowie Owens
fonte
Um bom argumento sobre os tipos indizíveis em C ++. No entanto, eu acho que é menos um motivo para adicionar inferência de tipo, do que um motivo para corrigir o idioma. No primeiro caso que você apresentar, o programador precisaria dar um nome à coisa para evitar o uso de inferência de tipo, portanto esse não é um exemplo forte. O segundo exemplo é forte apenas porque o C ++ proíbe explicitamente os tipos lambda de serem expressáveis, até mesmo a digitação com uso de typeofé inutilizada pela linguagem. E isso é um déficit da própria linguagem que deve ser corrigido na minha opinião.
precisa saber é