Como a digitação estática é realmente útil em projetos maiores?

9

Enquanto estava curioso na página principal do site de uma linguagem de programação de script, encontrei esta passagem:

Quando um sistema fica grande demais para ficar na sua cabeça, você pode adicionar tipos estáticos.

Isso me fez lembrar que em muitas guerras religiosas entre linguagens estáticas compiladas (como Java) e linguagens dinâmicas interpretadas (principalmente Python porque é mais usada, mas é um "problema" compartilhado entre a maioria das linguagens de script), uma das queixas de estática os fãs das linguagens digitadas em relação às linguagens dinamicamente digitadas é que eles não se adaptam bem a projetos maiores porque "um dia você esquecerá o tipo de retorno de uma função e precisará procurar, enquanto nas linguagens tipicamente estáticas tudo é declarado explicitamente ".

Eu nunca entendi declarações como esta. Para ser sincero, mesmo se você declarar o tipo de retorno de uma função, poderá e irá esquecê-lo depois de escrever muitas linhas de código e ainda precisará retornar à linha na qual é declarada usando a função de pesquisa de seu editor de texto para verificá-lo.

Além disso, como as funções são declaradas com type funcname()..., sem saber que typevocê terá que pesquisar sobre cada linha na qual a função é chamada, porque você só sabe funcname, enquanto em Python e similares você poderia procurar apenas def funcnameou o function funcnameque acontece apenas uma vez, em a declaração.

Além disso, com REPLs é trivial testar uma função para seu tipo de retorno com entradas diferentes, enquanto que com linguagens estáticas, você precisará adicionar algumas linhas de código e recompilar tudo apenas para saber o tipo declarado.

Portanto, além de conhecer o tipo de retorno de uma função que claramente não é um ponto forte das linguagens estáticas, como a digitação estática é realmente útil em projetos maiores?

user6245072
fonte
2
se você ler as respostas à outra pergunta, você provavelmente irá obter as respostas que precisa para um presente, eles são, basicamente, pedindo a mesma coisa de diferentes perspectivas :)
sara
11
Swift e playgrounds são um REPL de uma linguagem estaticamente tipada.
David11
2
Idiomas não são compilados, implementações são. A maneira de escrever um REPL para uma linguagem "compilada" é escrever algo que possa interpretar a linguagem, ou pelo menos compilar e executá-la linha por linha, mantendo o estado necessário por perto. Além disso, o Java 9 será enviado com um REPL.
Sebastian Redl
2
@ user6245072: Veja como fazer um REPL para um intérprete: leia o código, envie-o ao intérprete, imprima o resultado. Aqui está como criar um REPL para um compilador: leia o código, envie-o ao compilador, execute o código compilado , imprima o resultado. Fácil como torta. É exatamente isso que o FSi (o F♯ REPL), o GHCi (o REPL de GHC Haskell), o Scala REPL e o Cling fazem.
Jörg W Mittag

Respostas:

21

Além disso, com REPLs, é trivial testar uma função para seu tipo de retorno com entradas diferentes

Não é trivial. Não é trivial em tudo . É apenas trivial fazer isso para funções triviais.

Por exemplo, você pode definir trivialmente uma função em que o tipo de retorno depende inteiramente do tipo de entrada.

getAnswer(v) {
 return v.answer
}

Nesse caso, getAnswernão possui realmente um único tipo de retorno. Não há nenhum teste que você possa escrever que chame isso com uma entrada de amostra para saber qual é o tipo de retorno. Ele vai sempre depender do argumento real. Em tempo de execução.

E isso nem inclui funções que, por exemplo, executam pesquisas no banco de dados. Ou faça as coisas com base na entrada do usuário. Ou procure variáveis ​​globais, que são obviamente de um tipo dinâmico. Ou altere seu tipo de retorno em casos aleatórios. Sem mencionar a necessidade de testar todas as funções individuais manualmente todas as vezes.

getAnswer(x, y) {
   if (x + y.answer == 13)
       return 1;
   return "1";
}

Fundamentalmente, provar o tipo de retorno da função no caso geral é literalmente matematicamente impossível (Problema de Parada). A única maneira de garantir o tipo de retorno é restringir a entrada para que a resposta a essa pergunta não se enquadre no domínio do Problema da Parada, impedindo programas que não são prováveis, e é isso que a digitação estática faz.

Além disso, como as funções são declaradas com o tipo funcname () ..., sem o tipo de conhecimento, você terá que pesquisar sobre cada linha na qual a função é chamada, porque você só conhece o nome da função, enquanto em Python e similares você pode apenas procure def nome da função ou nome da função que só acontece uma vez na declaração.

Linguagens de tipo estático têm coisas chamadas "ferramentas". São programas que ajudam você a fazer coisas com o seu código-fonte. Nesse caso, eu simplesmente clicaria com o botão direito e em Ir para a definição, graças ao Resharper. Ou use o atalho do teclado. Ou apenas passe o mouse e ele me dirá quais são os tipos envolvidos. Eu não me importo nem um pouco com arquivos grepping. Um editor de texto por si só é uma ferramenta patética para editar o código-fonte do programa.

A partir da memória, def funcnamenão seria suficiente no Python, pois a função poderia ser redesignada arbitrariamente. Ou pode ser declarado repetidamente em vários módulos. Ou nas aulas. Etc.

e você ainda precisará retornar à linha em que foi declarada usando a função de pesquisa do seu editor de texto para verificá-la.

Procurar arquivos pelo nome da função é uma operação primitiva terrível que nunca deve ser necessária. Isso representa uma falha fundamental do seu ambiente e ferramentas. O fato de você considerar a necessidade de uma pesquisa de texto no Python é um ponto importante contra o Python.

DeadMG
fonte
2
Para ser justo, essas "ferramentas" foram inventadas em linguagens dinâmicas, e as linguagens dinâmicas as possuíam muito antes das linguagens estáticas. Ir para Definição, Conclusão de Código, Refatoração Automática etc. existia nos IDEs gráficos Lisp e Smalltalk antes que as linguagens estáticas tivessem gráficos ou IDEs, muito menos IDEs gráficos.
Jörg W Mittag
Sabendo o tipo de retorno de funções nem sempre dizer o que funções FAZER . Em vez de escrever tipos, você pode escrever testes de documentos com valores de exemplo. por exemplo, compare (palavras 'algumas palavras oue') => ['algumas', 'palavras', 'oeu'] com (sequência de palavras) -> [sequência], (zip {abc} [1..3]) => [(a, 1), (b, 2), (c, 3)] com seu tipo.
aoeu256 22/09/19
18

Pense em um projeto com muitos programadores que mudou ao longo dos anos. Você tem que manter isso. Há uma função

getAnswer(v) {
 return v.answer
}

O que diabos isso faz? O que é v? De onde vem o elemento answer?

getAnswer(v : AnswerBot) {
  return v.answer
}

Agora temos mais algumas informações -; precisa de um tipo de AnswerBot.

Se formos para um idioma baseado em classe, podemos dizer

class AnswerBot {
  var answer : String
  func getAnswer() -> String {
    return answer
  }
}

Agora podemos ter uma variável do tipo AnswerBote chamar o método getAnswere todo mundo sabe o que faz. Quaisquer alterações são capturadas pelo compilador antes de qualquer teste de tempo de execução. Existem muitos outros exemplos, mas talvez isso lhe dê a idéia?

daven11
fonte
11
Parece já mais claro - a menos que você indique que uma função como essa não tem razão de existir, mas é claro que é apenas um exemplo.
user6245072
Esse é o problema quando você tem vários programadores em um projeto grande, funções como essas existem (e pior), são coisas de pesadelos. também consideramos que funções em linguagens dinâmicas estão no espaço de nomes global; portanto, com o tempo, você pode ter algumas funções getAnswer - e elas funcionam e são diferentes porque são carregadas em momentos diferentes.
daven11
11
Eu acho que é um mal-entendido de programação funcional que causa isso. No entanto, o que você quer dizer com dizer que eles estão no espaço para nome global?
user6245072
3
"funções em idiomas dinâmicos são, por padrão, no espaço de nomes global", esse é um detalhe específico do idioma, e não uma restrição causada pela digitação dinâmica.
Sara
2
@ daven11 "Estou pensando em javascript aqui", de fato, mas outras linguagens dinâmicas têm namespaces / módulos / pacotes reais e podem alertá-lo sobre redefinições. Você pode estar generalizando demais um pouco.
Coredump
10

Você parece ter alguns conceitos errados sobre como trabalhar com grandes projetos estáticos que podem estar atrapalhando seu julgamento. Aqui estão algumas dicas:

mesmo se você declarar o tipo de retorno de uma função, poderá e irá esquecê-la depois de escrever muitas linhas de código e ainda precisará retornar à linha na qual é declarada usando a função de pesquisa do seu editor de texto para Confira.

A maioria das pessoas que trabalha com linguagens de tipo estaticamente usa um IDE para a linguagem ou um editor inteligente (como vim ou emacs) que possui integração com ferramentas específicas da linguagem. Geralmente, existe uma maneira rápida de encontrar o tipo de uma função nessas ferramentas. Por exemplo, com o Eclipse em um projeto Java, há duas maneiras pelas quais você normalmente encontra o tipo de um método:

  • Se eu quiser usar um método em outro objeto que não seja 'this', digite uma referência e um ponto (por exemplo someVariable.) e o Eclipse procurará o tipo someVariablee fornecerá uma lista suspensa de todos os métodos definidos nesse tipo; enquanto eu desço a lista, o tipo e a documentação de cada um são exibidos enquanto são selecionados. Observe que isso é muito difícil de obter com uma linguagem dinâmica, porque é difícil (ou, em alguns casos, impossível) para o editor determinar qual é o tipo someVariable, portanto, não é possível gerar a lista correta facilmente. Se eu quiser usar um método, thisbasta pressionar ctrl + space para obter a mesma lista (embora, neste caso, não seja tão difícil de obter para idiomas dinâmicos).
  • Se eu já tenho uma referência gravada em um método específico, posso mover o cursor do mouse sobre ele e o tipo e a documentação do método são exibidos em uma dica de ferramenta.

Como você pode ver, isso é um pouco melhor do que as ferramentas típicas disponíveis para linguagens dinâmicas (não que isso seja impossível em linguagens dinâmicas, pois algumas possuem uma funcionalidade IDE muito boa - o smalltalk é algo que chama a atenção - mas é mais difícil para uma linguagem dinâmica e, portanto, menos provável de estar disponível).

Além disso, como as funções são declaradas com o tipo funcname () ..., sem o tipo de conhecimento, você terá que pesquisar sobre cada linha na qual a função é chamada, porque você só conhece o nome da função, enquanto em Python e similares você pode apenas procure def nome da função ou nome da função que só acontece uma vez na declaração.

As ferramentas de linguagem estática normalmente fornecem recursos de pesquisa semântica, ou seja, eles podem encontrar a definição e referências a símbolos específicos com precisão, sem a necessidade de realizar uma pesquisa de texto. Por exemplo, usando o Eclipse para um projeto Java, posso destacar um símbolo no editor de texto e clicar com o botão direito do mouse e escolher 'ir para a definição' ou 'localizar referências' para executar uma dessas operações. Você não precisa procurar o texto de uma definição de função, porque seu editor já sabe exatamente onde está.

No entanto, o inverso é que procurar por uma definição de método por texto realmente não funciona tão bem em um grande projeto dinâmico como você sugere, pois pode haver facilmente vários métodos com o mesmo nome em um projeto e você provavelmente não tem. ferramentas prontamente disponíveis para desambiguar qual delas você está invocando (porque essas ferramentas são difíceis de escrever na melhor das hipóteses, ou impossíveis no caso geral), então você terá que fazer isso manualmente.

Além disso, com REPLs, é trivial testar uma função para seu tipo de retorno com entradas diferentes

Não é impossível ter um REPL para uma linguagem de tipo estaticamente. Haskell é o exemplo que vem à mente, mas também existem REPLs para outras linguagens de tipo estaticamente. Mas o ponto é que você não precisa executar o código para encontrar o tipo de retorno de uma função em uma linguagem estática - ela pode ser determinada pelo exame sem a necessidade de executar nada.

enquanto que nas linguagens estáticas, você precisará adicionar algumas linhas de código e recompilar tudo apenas para saber o tipo declarado.

As chances são de que, mesmo que você precisasse fazer isso, não precisaria recompilar tudo . A maioria das linguagens estáticas modernas possui compiladores incrementais que compilarão apenas a pequena parte do seu código que foi alterada, para que você possa obter feedback quase instantâneo para erros de tipo se você fizer um. O Eclipse / Java, por exemplo, destacará os erros de digitação enquanto você ainda os digita .

Jules
fonte
4
You seem to have a few misconceptions about working with large static projects that may be clouding your judgement.Bem, eu tenho apenas 14 anos e programa apenas a menos de um ano no Android, então é possível, eu acho.
user6245072
11
Mesmo sem um IDE, se você remover um método de uma classe em Java e houver coisas que dependem desse método, qualquer compilador Java fornecerá uma lista de todas as linhas que estavam usando esse método. No Python, ele falha quando o código em execução chama o método ausente. Uso Java e Python regularmente e adoro o Python pela rapidez com que você pode executar as coisas e as coisas legais que o Java não suporta, mas a realidade é que tenho problemas nos programas Python que simplesmente não acontecem com Java (direto). Refatorar em particular é muito mais difícil no Python.
21416 JimmyJames
6
  1. Porque os verificadores estáticos são mais fáceis para idiomas de tipo estaticamente.
    • No mínimo, sem recursos de linguagem dinâmica, se compilar, em tempo de execução não haverá funções não resolvidas. Isso é comum em projetos da ADA e C em microcontroladores. (Programas de microcontroladores ficam grandes às vezes ... como centenas de klocs grandes.)
  2. As verificações de referência de compilação estática são um subconjunto de invariantes de função, que em uma linguagem estática também podem ser verificados em tempo de compilação.
  3. Os idiomas estáticos geralmente têm mais transparência referencial. O resultado é que um novo desenvolvedor pode mergulhar em um único arquivo e entender um pouco do que está acontecendo, além de corrigir um bug ou adicionar um pequeno recurso sem precisar conhecer todas as coisas estranhas na base de código.

Compare com digamos, javascript, Ruby ou Smalltalk, onde os desenvolvedores redefinem a funcionalidade da linguagem principal em tempo de execução. Isso dificulta a compreensão do grande projeto.

Projetos maiores não têm apenas mais pessoas, eles têm mais tempo. Tempo suficiente para todos esquecerem ou seguirem em frente.

Curiosamente, um conhecido meu tem uma programação segura "Job For Life" no Lisp. Ninguém, exceto a equipe, pode entender a base de código.

Tim Williscroft
fonte
Anecdotally, an acquaintance of mine has a secure "Job For Life" programming in Lisp. Nobody except the team can understand the code-base.É tão ruim assim? A personalização que eles adicionaram não os ajuda a serem mais produtivos?
user6245072
@ user6245072 Pode ser uma vantagem para as pessoas que trabalham atualmente no local, mas dificulta o recrutamento de novas pessoas. Leva mais tempo para encontrar alguém que já conheça um idioma que não seja mainstream ou para ensiná-lo a um que ainda não conheça. Isso pode dificultar o dimensionamento do projeto quando for bem-sucedido ou a recuperação da flutuação - as pessoas se afastam, são promovidas para outras posições ... Depois de um tempo, também pode ser uma desvantagem para os próprios especialistas - uma vez que você tenha escrito alguma língua de nicho por mais ou menos uma década, pode ser difícil mudar para algo novo.
Hulk
Você não pode simplesmente usar um rastreador para criar testes de unidade a partir do programa Lisp em execução? Como no Python, você pode criar um decorador (advérbio) chamado print_args que recebe uma função e retorna uma função modificada que imprime seu argumento. Você pode aplicá-lo a todo o programa em sys.modules, embora uma maneira mais fácil de fazer isso seja usar o sys.set_trace.
aoeu256 22/09/19
@ aoeu256 Não estou familiarizado com os recursos do ambiente de tempo de execução Lisp. Mas eles usavam macros intensamente, para que nenhum programador de lisp normal pudesse ler o código; É provável que tentar fazer coisas "simples" no tempo de execução não funcione devido às macros alterarem tudo no Lisp.
Tim Williscroft
@ TimWilliscroft Você pode esperar até que todas as macros sejam expandidas antes de fazer esse tipo de coisa. O Emacs possui muitas teclas de atalho para permitir a expansão em linha de macros (e funções em linha talvez).
aoeu256 28/09/19
4

Eu nunca entendi declarações como esta. Para ser sincero, mesmo se você declarar o tipo de retorno de uma função, poderá e irá esquecê-lo depois de escrever muitas linhas de código e ainda precisará retornar à linha na qual é declarada usando a função de pesquisa de seu editor de texto para verificá-lo.

Não se trata de você esquecer o tipo de retorno - isso sempre vai acontecer. É sobre a ferramenta poder informar que você esqueceu o tipo de retorno.

Além disso, como as funções são declaradas com o tipo funcname()..., sem o tipo de conhecimento, você terá que pesquisar sobre cada linha na qual a função é chamada, porque você só sabe funcname, enquanto no Python e similares você poderia procurar def funcnameou o function funcnameque só acontece uma vez , na declaração.

Esta é uma questão de sintaxe, que não tem nenhuma relação com a digitação estática.

A sintaxe da família C é realmente hostil quando você deseja procurar uma declaração sem ter ferramentas especializadas à sua disposição. Outros idiomas não têm esse problema. Veja a sintaxe da declaração de Rust:

fn funcname(a: i32) -> i32

Além disso, com REPLs, é trivial testar uma função para seu tipo de retorno com entradas diferentes, enquanto que com linguagens de tipo estaticamente você precisará adicionar algumas linhas de código e recompilar tudo apenas para saber o tipo declarado.

Qualquer idioma pode ser interpretado e qualquer idioma pode ter um REPL.


Portanto, além de conhecer o tipo de retorno de uma função que claramente não é um ponto forte das linguagens estaticamente tipadas, como a tipagem estática é realmente útil em projetos maiores?

Eu responderei de uma maneira abstrata.

Um programa consiste em várias operações e essas operações são definidas da maneira que são devido a algumas suposições feitas pelo desenvolvedor.

Algumas suposições estão implícitas e outras são explícitas. Algumas suposições dizem respeito a uma operação próxima a elas, outras dizem respeito a uma operação fora delas. Uma suposição é mais fácil de identificar quando é expressa explicitamente e o mais próximo possível dos locais onde seu valor de verdade é importante.

Um bug é a manifestação de uma suposição que existe no programa, mas não é válida em alguns casos. Para rastrear um bug, precisamos identificar a suposição errônea. Para remover o bug, precisamos remover essa suposição do programa ou alterar algo para que a suposição realmente se mantenha.

Eu gostaria de categorizar suposições em dois tipos.

O primeiro tipo são as suposições que podem ou não ser mantidas, dependendo das entradas do programa. Para identificar uma suposição errônea desse tipo, precisamos procurar no espaço todas as entradas possíveis do programa. Usando suposições educadas e pensamento racional, podemos diminuir o problema e procurar em um espaço muito menor. Mas, ainda assim, à medida que um programa cresce um pouco, seu espaço de entrada inicial cresce a uma taxa enorme - até o ponto em que pode ser considerado infinito para todos os fins práticos.

O segundo tipo são as suposições que definitivamente valem para todas as entradas, ou são definitivamente erradas para todas as entradas. Quando identificamos uma suposição desse tipo como errônea, nem precisamos executar o programa ou testar nenhuma entrada. Quando identificamos uma suposição desse tipo como correta, temos menos um suspeito para nos preocupar quando rastreamos um bug ( qualquer bug). Portanto, há valor em ter o maior número possível de suposições pertencer a esse tipo.

Para colocar uma suposição na segunda categoria (sempre verdadeira ou sempre falsa, independente das entradas), precisamos de uma quantidade mínima de informações para estar disponível no local em que a suposição é feita. No código-fonte de um programa, as informações ficam obsoletas rapidamente (por exemplo, muitos compiladores não fazem análises interprocedurais, o que torna qualquer chamada um limite rígido para a maioria das informações). Precisamos de uma maneira de manter as informações necessárias atualizadas (válidas e próximas).

Uma maneira é ter a fonte dessas informações o mais próximo possível do local onde elas serão consumidas, mas isso pode ser impraticável para a maioria dos casos de uso. Outra maneira é repetir as informações com frequência, renovando sua relevância no código-fonte.

Como você já pode imaginar, os tipos estáticos são exatamente isso - faróis de informações de tipo espalhados pelo código-fonte. Essas informações podem ser usadas para colocar a maioria das suposições sobre correção de tipo na segunda categoria, o que significa que quase qualquer operação pode ser classificada como sempre correta ou sempre incorreta com relação à compatibilidade de tipo.

Quando nossos tipos estão incorretos, a análise economiza tempo, chamando a atenção do erro mais cedo do que tarde. Quando nossos tipos estão corretos, a análise economiza tempo, garantindo que, quando ocorrer um erro, possamos excluir imediatamente erros de tipo.

Theodoros Chatzigiannakis
fonte
3

Você se lembra do velho ditado "lixo dentro, lixo fora", bem, é isso que a digitação estática ajuda a evitar. Não é uma panacéia universal, mas o rigor sobre o tipo de dados que uma rotina aceita e retorna significa que você tem alguma garantia de que está trabalhando corretamente com ela.

Portanto, uma rotina getAnswer que retorna um número inteiro não será útil quando você tentar usá-la em uma chamada baseada em string. A digitação estática já está lhe dizendo para tomar cuidado, que você provavelmente está cometendo um erro. (e, com certeza, você poderá substituí-lo, mas precisará saber exatamente o que está fazendo e especificá-lo no código usando uma conversão. Geralmente, porém, você não deseja fazer isso - hackear um peg redondo em um buraco quadrado nunca funciona bem no final)

Agora você pode ir mais longe usando tipos complexos, criando uma classe que tem funcionalidade de minério. Você pode começar a repassá-los e, de repente, obterá muito mais estrutura em seu programa. Programas estruturados são aqueles que são muito mais fáceis de fazer funcionar corretamente e também mantêm.

gbjbaanb
fonte
Você não precisa fazer inferência estática de tipo (pylint), pode fazer inferência dinâmica de tipo chrislaffra.blogspot.com/2016/12/…, o que também é feito pelo compilador JIT do PyPy. Há também outra versão da inferência de tipo dinâmico, na qual um computador coloca aleatoriamente objetos simulados nos argumentos e vê o que causa um erro. O problema de parada não importa para 99% dos casos, se você demorar muito para interromper o algoritmo (é assim que o Python lida com recursão infinita, ele tem um limite de recursão que pode ser definido).
aoeu256 22/09/19