Como analisar uma linguagem de programação? Quais são os critérios para definir a melhor para cada situação? Como saber os critérios de linguagens pode mudar sua vida? Este post discute essas perguntas.
1. Linguagem de estimação
Além de bicho de estimação, algo corriqueiro atualmente é ter uma linguagem de programação de estimação. Você não precisa levar ela para passear todo dia, mas acaba a usando para tudo. Qualquer demanda de programação que tenha, dá um jeito de utilizá-la, mesmo que ela não seja apropriada.
Eu conheço um amigo que precisava fazer um software muito simples. Um aplicativo informativo para iOS composto de 5 telas e com navegação lateral de slide para os lados. Não tinha cadastro, objetos 3D, interações complexas, física, cálculos matemáticos que precisassem ser implementados. Ele fez em C# usando a Unity 3D. Uma game engine, que possui um motor gráfico embutido, que nesse caso não é usado. O aplicativo deveria ter 2MB e ficou com 50MB, porque nele existem as 5 telinhas, 1 script de programação e o motor gráfico padrão da Unity 3D.
Conhecer os critérios usados em projetos de linguagem de programação permite um maior embasamento para escolher linguagens adequadas para cada situação.
2. Efeito dominó
Sabe quando você aprende um novo idioma e descobre que uma palavra é parecida com um terceiro idioma, mas não com o seu? Por exemplo, bem-vindo em inglês é welcome, ou seja, nenhuma semelhança com a palavra em português. Depois que sei que welcome é bem vindo em inglês, descubro que bem vindo em alemão é wilkomen. Ela é parecida com o inglês, mas continua bem diferente do português. Ou seja, quanto mais idiomas aprendo, mais eu identifico semelhança e diferença entre eles e isso facilita o aprendizado (e em alguns casos deve confundir também, algo a ser perguntado para poliglotas).
Em programação, ocorre algo parecido. Quanto mais linguagens e recursos você aprende, mais identifica semelhança e diferença entre as linguagens. Ao compreender um novo recurso de uma linguagem, acaba pensando e comparando como fazer aquilo em outra. Isso aumenta a habilidade para aprender novas linguagens. Cada vez fica mais fácil e ocorre o que costumo afirmar: programador que é programador mesmo programa em qualquer linguagem. Ele pega a especificação, estuda, pesquisa e, em pouco tempo, consegue implementar seu algoritmo naquela linguagem sem nunca ter tido um contato maior com ela.
Conhecer os critérios de uma linguagem pode representar também conhece-la mais a fundo, aumentando a capacidade de entendimentos dos recursos da linguagem e consequentemente a capacidade de expressar algoritmos e soluções com ela.
Dada a importância de entender os critérios que norteiam a análise de uma linguagem, vamos a eles.
3. Legibilidade
Um dos critérios mais importantes de linguagens de programação é a legibilidade, que é facilidade com a qual os programas podem ser lidos e entendidos.
Com o conceito de ciclo de vida de um software, a codificação inicial foi relegada a um papel menor, e a manutenção foi reconhecida como a parte principal do ciclo, principalmente em termos de custo. A manutenção é determinada, em grande parte, pela legibilidade de programas, que passou a ser uma medida importante na qualidade dos programas e das linguagens de programação.
A sintaxe de uma linguagem de programação tem bastante impacto na legibilidade. Por sintaxe entenda todos os elementos da linguagem e suas regras para combiná-los. A forma de definir identificadores, como variáveis, influencia a legibilidade. Por exemplo, na linguagem PHP, as variáveis devem começar com o símbolo $. Tudo o que vier com esse símbolo é uma variável. Na linguagem C não é preciso usar nenhum símbolo inicial. Outros fatores de sintaxe associados a legibilidade são como e quais palavras especiais ou reservadas são usadas e como ela está relacionada com a semântica (significado).
Vamos ver outros fatores que influenciam a legibilidade.
3.1 Muitos recursos de escrita
Existem diversos fatores que afetam a legibilidade. Por exemplo, a multiplicidade de recursos pode ser um problema para a legibilidade. Na linguagem Java e em C++, por exemplo, é possível incrementar uma variável de 4 formas diferentes. Considere a variável a com quatro possibilidades de comandos para incrementar em 1 seu valor:
a = a + 1;
a += 1;
a++;
++a;
Apesar das duas últimas terem significados diferentes, ambas resultam no acréscimo do valor armazenado na variável a em 1 unidade. Imagine um código que alterna entre esses padrões para acréscimo de valor. Isso faria o código ficar confuso e difícil de ser lido. Mesmo não sendo uma boa prática mudar o padrão, a linguagem permite isso, que pode afetar a leitura do código.
3.2 Poucos recursos de escrita
O contrário, porém, também é um problema. Imagine uma linguagem com poucos recursos de escrita, como Assembly, por exemplo. Ela aceita comandos primitivos, que devem ser executados um a um. Ao invés de usar uma expressão com uma única linha como a = 10 + 20, é preciso usar várias linhas para realizar essa soma.
Primeiro, um comando aloca um registrador ou espaço na memória. Depois, é necessário colocar o valor 10 no espaço alocado (equivalente a variável). Em seguida, o valor 20 em outro espaço e só então, somá-los. Usam-se operadores diferentes para alocar espaço, copiar valor no espaço e somar dois valores armazenados em espaços de memória. É uma linguagem de montagem e pouco legível devido ao fato de ser preciso várias linhas de código para uma simples atribuição. Esse é um dos motivos, na prática, usados para definir uma linguagem de alto nível de uma de baixo nível. A de alto nível, no fundo, converte tudo o que você digita para uma de baixo nível, facilmente entendida pela máquina. Porém,. para quem programa, a legibilidade de uma linguagem de alto nível é muito melhor.
3.3 Sobrecargas
Linguagens que permitem a sobrecarga de operadores e de métodos também podem comprometer a legibilidade se não forem usadas de forma intuitiva. Imagine uma sobrecarga do operador de adição para o cálculo do produto vetorial ao invés da soma de dois vetores. Certamente vai confundir quem ler o código e não estiver avisado dessa definição.
4. Ortogonalidade
Ortogonalidade em uma linguagem de programação significa que um conjunto relativamente pequeno de construções primitivas pode ser combinado a um número relativamente pequeno de formas para construir as estruturas de controle e de dados da linguagem. Uma falta de ortogonalidade leva a exceções às regras de linguagem. Por exemplo, deve ser possível, em uma linguagem de programação que possibilita o uso de ponteiros, definir que um aponte para qualquer tipo específico definido na linguagem. Entretanto, se não for permitido aos ponteiros apontar para vetores, muitas estruturas de dados potencialmente úteis definidas pelos usuários não poderiam ser definidas.
4.1 Simplicidade
A ortogonalidade é fortemente relacionada à simplicidade: quanto mais ortogonal o projeto de uma linguagem, menor é o número necessário de exceções às regras da linguagem. Menos exceções significam um maior grau de regularidade no projeto, o que torna a linguagem mais fácil de aprender, ler e entender.
Como exemplos da falta de ortogonalidade em uma linguagem de alto nível, considere as seguintes regras e exceções em C. Apesar de C ter duas formas de tipos de dados estruturados, vetores e registros (structs), os registros podem ser retornados por funções, mas os vetores não. Um membro de uma estrutura pode ser de qualquer tipo de dados, exceto void ou uma estrutura
do mesmo tipo. Um elemento de um vetor pode ser qualquer tipo de dados, exceto void ou uma função. Parâmetros são passados por valor, a menos que sejam vetores, o que faz com que sejam passados por referência (porque a ocorrência de um nome de um vetor sem um índice em um programa em C é interpretada como o endereço do primeiro elemento desse vetor).
4.2 Contexto
O significado de um recurso de linguagem ortogonal é independente do contexto de sua aparição em um programa. Como um exemplo da dependência do contexto, considere a seguinte
expressão em C
a + b
Ela significa que os valores de a e b são obtidos e adicionados juntos. Entretanto, se a for um ponteiro, afeta o valor de b. Por exemplo, se a aponta para um valor de ponto flutuante que ocupa quatro bytes, o valor de b deve ser ampliado – nesse caso, multiplicado por 4 – antes que seja adicionado a a. Logo, o tipo de a afeta o tratamento do valor de b. O contexto de b afeta seu
significado.
5. Tipos de dados
A presença de mecanismos adequados para definir tipos e estruturas de dados é outro auxílio significativo à legibilidade. Por exemplo, suponha que um tipo
numérico seja usado como uma flag porque não existe nenhum tipo booleano na linguagem. Em tal linguagem, poderíamos ter uma atribuição como:
timeOut = 1
O significado dessa sentença não é claro. Em uma linguagem que inclui tipos booleanos, teríamos:
timeOut = true
O significado dessa sentença é perfeitamente claro.
6. Facilidade de escrita
A facilidade de escrita é o critério que diz quão fácil é escrever uma solução usando determinada linguagem de programação. O primeiro fator é o contexto do software a ser desenvolvido. Não dá para comparar uma linguagem que não foi projetada para determinada função com uma que foi e medir a facilidade de escrita das duas. O critério depende da área do problema a ser resolvido. Por exemplo, as facilidades de escrita do Visual BASIC (VB) e do C são drasticamente diferentes para criar um programa com uma interface gráfica com o usuário, para o qual o VB é ideal. Suas facilidades de escrita também são bastante diferentes para a escrita de programas de sistema, como um sistema operacional, para os quais a linguagem C foi projetada.
Este critério não deve ser confundido com a legibilidade, embora a maioria das características que afetam a legibilidade, também afetam a facilidade de escrita. Ao escrever um código é normal lê-lo várias vezes. É assim que funciona também em uma redação, mas enquanto a legibilidade é capacidade de ler o código, a facilidade de escrita está relacionada aos recursos existentes para escrever o código. Já discutimos acima a falta e o excesso de recursos, demonstrados também com a sobrecarga de métodos e operadores.
6.1 Simplicidade e ortogonalidade
Se uma linguagem tem um grande número de construções, alguns programadores não estarão familiarizados com todas. Essa situação pode levar ao uso incorreto de alguns recursos e a uma utilização escassa de outros que podem ser mais elegantes ou mais eficientes (ou ambos) do que os usados. A simplicidade afeta a facilidade de escrita de uma linguagem.
Um número menor de construções primitivas e um conjunto de regras consistente para combiná-las (isso é, ortogonalidade) é muito melhor do que diversas construções primitivas. Um programador pode projetar uma solução para um problema complexo após aprender apenas um conjunto simples de construções primitivas. Por outro lado, muita ortogonalidade pode prejudicar a facilidade de escrita. Erros em programas podem passar despercebidos quando praticamente quaisquer combinações de primitivas são legais. Isso pode levar a certos absurdos no código que não podem ser descobertos pelo compilador.
Uma linguagem como Java possui todas sua estrutura baseada em classes, definindo claramente uma regra que novas classes podem ser feitas e estabelecendo os limites para ela. Uma das regras da linguagem Java quanto a classes é aceitar apenas herança simples, ou seja, uma classe não pode derivar de mais de uma classe (quando isso é preciso, Java possui o recurso de usar interfaces). A linguagem C++ possui tanto a estrutura structure, quanto classes e aceita herança múltipla. Recursos que, olhando o fator de facilidade de escrita, a tornam mais difícil quando comparada a Java.
6.2 Suporte à abstração
Abstração é um fator fundamental no projeto de uma linguagem de programação. O grau de abstração permitido por uma linguagem de programação e a naturalidade de sua expressão são importantes para sua facilidade de escrita. As linguagens de programação podem oferecer suporte a duas categorias de abstrações: processos e dados.
Como um exemplo de abstração de dados, considere uma árvore binária que armazena dados inteiros em seus nós. Tal árvore poderia ser implementada em uma linguagem que não oferece suporte a ponteiros e gerenciamento de memória dinâmica usando um monte (heap), como na linguagem Fortran, com o uso de três vetores inteiros paralelos, onde dois dos inteiros são usados como índices para especificar nós filhos. Em C++ e Java, essas árvores podem ser implementadas utilizando uma abstração de um nó de árvore na forma de uma simples classe com dois ponteiros (ou referências) e um inteiro. A naturalidade da última representação torna muito mais fácil escrever um programa que usa árvores binárias nessas linguagens do que em Fortran.
O suporte geral para abstrações é um fator importante na facilidade de escrita de uma linguagem.
7. Confiabilidade
Um programa é dito confiável quando está de acordo com suas especificações em todas as condições. A confiabilidade de uma linguagem está associada aos testes executados por ela os limites impostos no projeto da linguagem.
7.1 Ponteiros
Por exemplo, a linguagem C e C++ utilizam o recurso de ponteiros. Ponteiro é uma referência a um endereço de memória. O fato de permitir trabalhar com ponteiros implica que seja possível acessar regiões da memória que não estão previamente alocadas para o programa, gerando problemas da ordem de segurança e também tornando a legibilidade e escrita mais difíceis.
A linguagem Java não permite ao programador acessar ou usar ponteiros como em C++. Ao invés de permitir o acesso a posições de memória, ela permite apenas a referência. Internamente, ela usa ponteiros com endereços de memória, mas não permite ao programador seu uso direto. Isso aumenta a confiabilidade da linguagem Java comparada com a linguagem C e C++.
7.2 Verificações
Outro exemplo é a checagem de algumas condições antes de gerar o novo programa com o código. Ao declarar um array com 10 posições e atribuir um valor a uma posição inexistente, a linguagem C++ aceita e só gera o erro em tempo de execução. A linguagem Java não aceita. Ela confere todas as posições declaradas do vetor.
A verificação de tipos é um fator importante na confiabilidade. Ela representa o processo de detectar erros de tipos em um programa, tanto por parte do implementador quanto durante a execução de um programa. Como a verificação de tipos em tempo de execução é cara, a verificação em tempo de compilação é mais desejável. Além disso, quanto mais cedo os erros nos programas forem detectados, menos caro é fazer todos os reparos necessários. O projeto de Java requer verificações dos tipos de praticamente todas as variáveis e expressões em tempo de compilação. Isso praticamente elimina erros de tipos em tempo de execução em programas Java.
A linguagem Javascript é famosa por não dar pouquíssimos erros de verificação. Isso acaba sendo um problema porque, quando o programa gera um resultado inesperado, é mais difícil encontrar o erro com o programa rodando errado do que ser avisado de um erro semântico por exemplo e saber exatamente o que corrigir.
7.3 Tratamento de exceções
A habilidade de um programa de interceptar erros em tempo de execução (além de outras condições não usuais detectáveis pelo programa), tomar medidas corretivas e então continuar é uma ajuda óbvia para a confiabilidade. Tal facilidade é chamada de tratamento de exceções. Ada, C++ e Java incluem diversas capacidades para tratamento de exceções, mas tais
facilidades são praticamente inexistentes em muitas linguagens amplamente usadas, como C e Fortran.
7.4 Apelidos
Em uma definição bastante informal, apelidos são permitidos quando é possível ter um ou mais nomes para acessar a mesma célula de memória. Atualmente, é amplamente aceito que o uso de apelidos é um recurso perigoso em uma linguagem de programação. A maioria das linguagens permite algum tipo de apelido – por exemplo, dois ponteiros configurados para apontarem para a mesma variável, o que é possível na maioria das linguagens. O programador deve sempre lembrar que trocar o valor apontado por um dos dois ponteiros modifica o valor referenciado pelo outro.
Em algumas linguagens, apelidos são usados para resolver deficiências nos recursos de abstração de dados. Outras restringem o uso de apelidos para aumentar sua confiabilidade.
8. Custo
O termo custo de uma linguagem de programação é usado como a somatória de todos os critérios e também para falar dos custos gerados a partir de cada conjunto de critérios. Assim, o custo total de uma linguagem de programação é uma função de muitas de suas características. Confira a lista abaixo com alguns custos:
- O custo de treinar programadores para usar a linguagem é uma função da simplicidade, da ortogonalidade da linguagem e da experiência dos programadores. Apesar de linguagens mais poderosas não necessariamente serem mais difíceis de aprender, normalmente elas o são.
- O custo de escrever programas na linguagem. Essa é uma função da facilidade de escrita da linguagem, a qual depende da proximidade com o propósito da aplicação em particular. Os esforços originais de projetar e implementar linguagens de alto nível foram dirigidos pelo desejo de diminuir os custos da criação de software.
** Tanto o custo de treinar programadores quanto o custo de escrever programas em uma linguagem podem ser reduzidos significativamente em um bom ambiente de programação.
- O custo de compilar programas na linguagem.
- O custo de executar programas escritos em uma linguagem é amplamente influenciado pelo projeto dela. Uma linguagem que requer muitas verificações de tipos em tempo de execução proibirá uma execução rápida de código, independentemente da qualidade do compilador. Apesar de eficiência de execução ser a principal preocupação no projeto das primeiras linguagens, atualmente é considerada menos importante.
- o custo de manter programas, que inclui tanto as correções quanto as modificações para adicionar novas funcionalidades. O custo da manutenção de software depende de um número de características de linguagem, principalmente da legibilidade. Como que a manutenção é feita em geral por indivíduos que não são os autores originais do programa, uma legibilidade ruim pode tornar a tarefa extremamente desafiadora.
9. Considerações finais
A maioria dos critérios não é precisamente definida nem exatamente mensurável. Independentemente disso, são conceitos úteis e fornecem ideias valiosas para o projeto e para a avaliação de linguagens de programação.
Algumas linguagens que eventualmente tenham critérios teoricamente ruins comparados as de outras, podem facilmente ser usadas por um programador que sabe seus prós e contras. Por exemplo, para um programador experiente de C++, pouco importa que a linguagem C++ é pouco confiável quanto as verificações. Ele(a) está acostumado(a) a não cometer os possíveis erros perigosos que a linguagem permite.
Além disso, os critérios de projeto de linguagem têm diferentes pesos quando vistos de diferentes perspectivas. Implementadores de linguagens estão preocupados principalmente com a dificuldade de implementar as construções e recursos da linguagem. Os usuários estão preocupados primeiramente com a facilidade de escrita e depois com a legibilidade. Os projetistas são propensos a enfatizar a elegância e a habilidade de atrair um grande número de usuários. Essas características geralmente entram em conflito.
Referência
[1] – Sebesta, Robert W. Concepts of Programming Languages, 11ª ed. Pearson, 2015.