Tipos e testes

fevereiro 13, 2017 5:00 pm Publicado por Deixe um comentário

As respostas para o meu artigo O caminho das trevas têm sido divertidas. Isso tem variado de efusiva a uma discordância categórica. E também provocou alguns insultos abusivos. Eu aprecio a paixão. Um debate agradável é sempre a melhor maneira de aprender. Quanto aos insultadores: vocês precisam sair do porão da casa de sua mãe e começar uma vida.

Para ser claro, e correndo o risco de ser repetitivo, aquele texto não foi uma crítica à tipagem estática. Estou gostando bastante da tipagem estática. Eu passei os últimos 30 anos trabalhando com linguagens de tipagem estática e acabei me acostumando a elas.

Minha intenção, com aquele artigo, foi reclamar sobre o quão longe o pêndulo oscilou. Acho que a tipagem estática de Swift e Kotlin balançou para longe demais na direção dos tipos checados estaticamente . Eles, em minha humilde opinião, passaram do ponto onde a troca entre expressividade e restrição é lucrativa.

Uma das respostas mais comuns ao texto foi: “Ei, cara, tipo … Os tipos são testes!”.

Não, tipos não são testes. Type systems não são testes. A verificação de tipo não está fazendo testes. Aqui está o motivo.

Um programa de computador é uma especificação de comportamento. Sua finalidade é fazer uma máquina se comportar de um certo modo. O programa consiste nas instruções que a máquina segue. A soma dessas instruções é o comportamento do programa.

Tipos não especificam comportamento. Tipos são restrições colocadas pelo programador sobre os elementos textuais do programa. Essas restrições reduzem o número de maneiras que diferentes partes do programa podem referir-se uma à outra.

Agora, esse tipo de sistema de restrição pode ser muito útil na redução da incidência de erros num programa. Se você especificar que a função “f” deve ser chamada com um “Int“, o sistema irá garantir que nenhuma outra parte do programa irá chamar “f” com um “Double“. Se tal erro escapar para um programa em execução (como era comum nos bons velhos tempos C), isso provavelmente resultaria em um erro de execução.

Você pode, portanto, dizer que o type system é uma espécie de “teste” que falha para todas as invocações inapropriadas de “f“. Eu poderia aceitar esse ponto, exceto por uma coisa – a forma como “f” é chamado não tem nada a ver com o comportamento necessário do sistema. Pelo contrário, é um teste de uma restrição arbitrária imposta pelo programador. Uma restrição que foi provavelmente do ponto de vista dos requisitos do sistema.

Os requisitos do sistema provavelmente não dependem de o argumento de “f” ser um “Int“. Poderíamos muito provavelmente mudar a declaração de “f” para receber um “Double“, sem afetar o comportamento do programa.

Então, o que o type system está verificando não é o comportamento externo do programa. Ele está verificando somente a consistência interna do programa.

Mas isso não é pouca coisa. Escrever um programa que é internamente consistente é muito importante. Incoerências e ambiguidades no programa podem levar a maus comportamentos de todos os tipos. Então eu não quero minimizar tudo isso.

Por outro lado, a autoconsistência interna não significa que o programa apresenta o comportamento correto. Comportamento e autoconsistência são conceitos ortogonais. Programas com bom comportamento podem ser, e foram, escritos em linguagens de alta ambiguidade e baixa consistência interna. Programas com mal comportamento foram escritos em linguagens que são profundamente autoconsistentes e toleram poucas ambiguidades.

Então, quão internamente consistente precisamos que o programa seja? Será que toda a linha precisa ter exatamente 60 caracteres? As indentações devem ser sempre múltiplos de dois espaços? Todo número de ponto flutuante deve ter um ponto? Todos os números positivos devem começar com um +? Ou devemos permitir certas ambiguidades no nosso programa e permitir que a linguagem faça suposições que os resolvam? Devemos nos tranquilizar em relação à especificidade da linguagem e deixá-la tolerar certas ambiguidades facilmente solucionáveis? Devemos permitir isso, mesmo se, às vezes, essas ambiguidades forem resolvidas de forma incorreta?

Claramente, todas as linguagens escolhem a última opção. Nenhuma linguagem força o programa a ser absolutamente explícito e autoconsistente. Na verdade, criar tal linguagem provavelmente seria impossível. E, mesmo se não fosse, provavelmente seria impossível usar. Precisão absoluta e consistência nunca foram, nem devem ser, o objetivo.

Então, de quanta autoconsistência interna precisamos? Seria fácil dizer que precisamos de tanto quanto pudermos ter. Pode parecer óbvio que quanto mais explícita e internamente consistente uma linguagem seja, menos defeitos os programas escritos nessa linguagem terão. Mas isso é verdade?

O problema com o aumento do nível de precisão e consistência interna é que isso implica um aumento do número de restrições. Mas as restrições têm de ser especificadas, e especificação exige notação. Portanto, assim como o número de restrições cresce, também cresce a complexidade das notações. A sintaxe e a semântica de uma linguagem crescem em relação à autoconsistência interna e especificidade da linguagem.

Conforme a notação e a semântica crescem em complexidade, a chance de consequências não intencionais cresce. Entre suas piores consequências estão as violações do princípio de “Open-Closed“.

Imagine que há uma linguagem chamada TDP que é essencialmente autoconsistente e específica. Em TDP, cada linha de código é consistente e específica com outra linha de código. Uma mudança em uma linha força uma alteração em todas as outras linhas, a fim de manter essa consistência e especificidade.

Realmente existem linguagens assim? Não; mas quanto mais segura uma linguagem for, mais internamente consistente e específica ela força os programadores a serem, e mais se aproxima dessa condição da TDP.

Considere a palavra-chave “const” em C++. Quando eu estava começando a aprender C++, eu não a utilizava. Estava muito acima de tudo o que havia para aprender. Mas conforme eu ganhei conhecimento e conforto com a linguagem, veio o dia em que usei o meu primeiro “const“. E fui a fundo, corrigindo um erro de compilação após o outro, mudando centenas e centenas de linhas de código, até que o sistema em que eu estava trabalhando utilizava corretamente “const“.

Eu parei de usar “const” por causa dessa experiência? Não, claro que não. Eu me certifiquei de que eu sabia, de antemão, quais campos e funções seriam “const“. Isso exigiu muito projeto inicial, mas que valia a alternativa. Isso fez o problema desaparecer? Claro que não. Eu frequentemente estou percorrendo o sistema colocando “const” em todo lugar.

TDP é uma boa condição? Você quer ter de mudar cada linha de código toda vez que alguma coisa for alterada? Claro que não. Isso viola a OCP, e criaria um pesadelo para manutenção.

Talvez você pense que eu estou criando um argumento do espantalho. Afinal, TDP não existe. Meu ponto, porém, é que Swift e Kotlin deram um passo nessa direção indesejável. É por isso que eu o chamei de O caminho das trevas.

Cada passo por esse caminho aumenta a dificuldade de utilização e manutenção da linguagem. Cada passo por esse caminho obriga os usuários da linguagem a fazerem seus modelos de tipo antecipadamente, porque mudá-los mais tarde seria muito custoso. Cada passo por esse caminho nos força de volta ao regime do grande design antecipado.

Mas isso significa que nunca devemos tomar sequer um único passo por esse caminho? Isso significa que as nossas linguagens não devem ter tipos e nenhuma autoconsistência interna específica? Todos nós devemos programar em Lisp?

(Foi uma piada, todos vocês que vivem no porão da casa de sua mãe podem guardar seus insultos para si mesmos, por favor, e fiquem fora de meu gramado. Quanto ao Lisp, a resposta é: Sim, nós provavelmente deveríamos estar todos programando em Lisp; mas por razões diferentes.)

Segurança de tipo tem uma série de benefícios que, a princípio, superam os custos. Poucos passos por esse caminho escuro nos permitem recolher alguns frutos bastante interessantes. Podemos ganhar uma quantidade razoável de especificidade e autoconsistência sem grandes violações da OCP. Modelos de tipo também podem aumentar a expressividade e a legibilidade. E os modelos de tipo certamente ajudam as IDEs com refatorações e outras operações mecânicas.

Mas há um ponto de equilíbrio após o qual cada passo nesse caminho escuro aumenta o custo sobre o benefício.

Eu acho que Java e C# têm feito um trabalho razoável pairando perto do ponto de equilíbrio (se você ignorar a sintaxe horrível para os genéricos, e a restrição ridícula contra a herança múltipla). Na minha opinião, essas linguagens foram um pouco longe demais, mas os custos de segurança de tipo não são altos demais para tolerar. Ruby, Groovy, e JavaScript, por sua vez, pairam sobre o outro lado do ponto de equilíbrio. Elas são, talvez, um pouco permissivas demais, um pouco ambíguas demais (alguém realmente entende o gráfico sub-objeto no Ruby?).

Então, um pouco de segurança de tipo, como um pouco de sal, é uma coisa boa. Demais, por outro lado, pode ter consequências indesejáveis.

Será que cada passo no caminho das trevas significa que você pode ignorar um certo número de testes unitários? Será que a programação em linguagens do caminho das trevas significa que você não tem que testar tanto?

Não. Mil vezes: NÃO. Modelos de tipo não especificam comportamento. A correção do seu modelo de tipo não tem qualquer influência sobre a correção do comportamento que você especificou. Na melhor das hipóteses, o sistema de tipagem irá prevenir contra algumas falhas mecânicas de representação (por exemplo, “Double” vs. “Int“), mas você ainda tem que especificar cada parte do comportamento, e você ainda tem que testar cada parte do comportamento.

Então, não, type systems não diminuem a carga de testes. Nem mesmo um pouquinho. Mas eles podem prevenir contra alguns erros que os testes unitários não poderiam ver (por exemplo, “Double” vs. “Int“).

***

Uncle Bob faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://blog.cleancoder.com/uncle-bob/2017/01/13/TypesAndTests.html.

Source: IMasters

Categorizados em:

Este artigo foi escrito pormajor

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *