Quando eu apresento a minha pesquisa sobre linguagens de programação, as pessoas geralmente me perguntam “por que você precisa de uma nova linguagem de programação para resolver esse problema? Por que não apenas implementá-la como uma biblioteca?”. Ou me perguntam “por que você não o implementou como uma extensão para {alguma linguagem de programação}? Neste artigo, eu tentarei explicitar os objetivos e as motivações por trás do design da linguagem. Eles são totalmente formulados a partir do meu próprio background na área, então posso perder coisas importantes sobre as quais não pensei.
Neste artigo, irei explicitar quatro objetivos primários que podem conduzir o processo de design de linguagem:
- Linguagem como um mecanismo de abstração sintática: para reduzir o código clichê repetitivo que não pode ser abstraído do uso de outras linguagens, crie mecanismos abstratos intrínsecos.
- Linguagem como uma modeladora de pensamento: para induzir a mudança de paradigma em como estruturar um software (mudando “caminho de menor resistência”).
- Linguagem como um simplificador: para transformar um paradigma existente em algo que exalte apenas suas partes essenciais, muitas vezes para aumentar a compreensão e o insight.
- Linguagem que aplica a lei: para garantir propriedades importantes ou constantes, possivelmente para facilitar a inferência de propriedades mais úteis dos programas.
Esses objetivos não estão necessariamente sempre presentes e explícitos na mente de um designer de linguagem quando ele ou ela começa a desenvolver uma nova. Muito pelo contrário, acredito que muitas vezes eles estão presentes implicitamente, e podem mudar à medida que a linguagem cresce. Eles também não são exclusivos, e diferentes partes da linguagem podem ser guiadas por objetivos diferentes. A seguir, irei abstrair de tais detalhes do processo de design de linguagens e irei discutir cada um desses desafios separadamente.
Linguagem como um mecanismo de abstração sintática
Parece para mim que esta é a razão mais comum pela qual as pessoas buscam por novas linguagens de programação. Muitas vezes, o objetivo é fornecer uma nova sintaxe leve e agradável que permite que os programadores façam as mesmas coisas que antes, mas com menos código. Acho que isso explica parcialmente a mudança de popularidade do Java para Scala. Linguagens com domínio específico muitas vezes, mas não sempre, entram nessa categoria.
Essa visão leva à crença de que quanto mais poderosos os mecanismos de abstração sintáticas de uma linguagem são, menor é a necessidade de escapar para novas linguagens para evitar clichês. Macros na família Lips de linguagens me vêm à cabeça. Como os macros conseguem abstrair virtualmente qualquer padrão sintático nessas linguagens, um programador Lips muitas vezes pensa em uma linguagem de domínio específico como simplesmente um conjunto de macros e não vê necessidade de escapar para uma linguagem externa.
Linguagens de script geralmente têm o objetivo de abstração sintática, uma vez que elas muitas vezes abordam uma sintaxe bastante leve (considere, por exemplo, o uso do Python da identação para agrupamento, ou a concisão do I/O e a manipulação de strings virtualmente em todas as linguagens de script). Ainda assim, essas linguagens muitas vezes têm outros objetivos, tais como aumentar ainda mais a produtividade ao reduzir a quantidade de código necessária para que um programa seja executado. Parece que as linguagens de script geralmente não são inovadoras no sentido do paradigma que elas defendem, mas são muitas vezes fortemente influenciadas pelas linguagens “modeladora de pensamento” e “simplificadora”, discutidas abaixo.
Se fôssemos fazer uma analogia com as linguagens naturais, usar uma linguagem como um mecanismo de abstração sintática é como mudar o vocabulário de uma linguagem natural, por exemplo inventar uma nova palavra para um conceito já existente. Uma linguagem de domínio específico em computação é como um jargão técnico em outras profissões.
Linguagem como uma modeladora de pensamento
Citando Alan Perlis: “uma linguagem que não afeta a maneira que você pensa sobre programação não vale a pena conhecer”.
O objetivo de uma linguagem modeladora de pensamento é mudar a maneira como um programador pensa sobre a estruturação de seu programa. Os blocos básicos de construção fornecidos por uma linguagem de programação, assim com as maneiras que elas são capazes (ou não) de ser combinados, tendem a levar os programadores por um caminho de “menor resistência”, para certas unidades de resistência. Por exemplo, um estilo imperativo de programação é definitivamente o caminho de menor resistência em C. É possível escrever programas funcionais em C, mas como C não o faz pelo caminho de menor resistência, a maioria dos programas em C não será funcional.
Falando nisso, linguagens de programação funcionais são um bom exemplo de linguagens modeladoras de pensamento. Ao tirar a atribuição da caixa de ferramentas básica do programador, a linguagem realmente força os programadores oriundos de uma linguagem imperativa a mudar seus hábitos de codificação. Não estou pensando somente em linguagens puramente funcionais com o Haskell. Linguagens como ML e Clojure tornam o caminho da programação funcional o de menor resistência, mas elas não eliminam completamente os efeitos colaterais. Ao invés disso, ao meramente tirar a ênfase deles, um programa escrito nessas linguagens pode ser caracterizado como um mar de imutabilidade com ilhas de mutabilidade, em oposição a um mar de mutabilidade com ilhas de mutabilidade. Essa mudança sutil muitas vezes faz com que seja vastamente mais fácil raciocinar sobre o programa.
O modelo atual de Erlang baseado em processos isolados comunicando-se por mensagens é outro exemplo de design de linguagem que leva a uma estrutura radicalmente diferente de programa, quando comparado com modelos populares de multi-tarefas . O “GOTO” de Dijkstra é considerado “perigoso” e os Processos de Comunicação Sequenciais do Hoare são exemplos pioneiros do uso do design da linguagem para remodelar nossos pensamentos sobre programação. Outro exemplo é o Fortress, que quer nos direcionar em direção à escrita de programas paralelos (paralelizados) por padrão.
Expandindo a analogia com linguagens naturais, as linguagens como modeladoras de pensamento não têm o objetivo de mudar o vocabulário ou a gramática, mas sim de primariamente mudar os conceitos que discutimos. Erlang herda a maioria de sua sintaxe do Prolog, mas os conceitos do Erlang (processos, mensagens) são vastamente diferentes do Prolog (unificações, fatos e regras, backtracking). Como um pesquisador de linguagens de programação, eu realmente estou convencido de que a linguagem molda o pensamento.
Linguagem como um simplificador
Ou como a programação orientada a objeto no Smalltalk é qualitativamente diferente da programação orientada para o objeto no C++.
Às vezes, a crescente complexidade de linguagens de programação existentes leva os designers de linguagem a criar novas linguagens que estão sob o mesmo paradigma de programação, mas com o objetivo explícito de minimizar a complexidade e de maximizar a consistência, a regularidade e a uniformidade (resumindo, integridade conceitual).
Linguagens que eu classificaria como “simplificadoras” incluem Scheme e Smalltalk (apesar de que Smalltalk é razoavelmente também uma linguagem modeladora de pensamento). Self é um simplificador notável, uma vez que foi criado com o objetivo de simplificar o Smalltalk, uma linguagem que já tinha sido criada com simplicidade e uniformidade. De certa forma, até o Java pode ser pensado como um simplificador do C++, como foi observado por Mark S. Miller na sua palestra no Emerging Languages Camp, em 2010.
Outros exemplos dessa escola de pensamento onde “menos é mais” são álgebras de processos (o CSP do Hoare é um exemplo) e cálculos formais (como Abadi e o object calculi de Cardelli). Um objetivo explícito dessas linguagens formais é minimizar o número de recursos não-composicionais, minimizando o número de “exceções” que devem ser aprendidas.
Continuando com nossa analogia, usar uma linguagem como simplificador é como mudar a gramática de uma linguagem humana para que ela se torne mais consistente, contendo menos exceções. O esperanto é um exemplo de uma linguagem humana artificial que foi criada com esse objetivo em mente. Ela, como muitas linguagens de programação simples e regulares, falhou ao não conseguir vasta adoção, mas isso é assunto para outro artigo.
Linguagem que aplica a lei
Para falar diretamente: uma grande quantidade de bibliotecas não irá tornar o C uma linguagem segura de memória. Da mesma maneira, uma grande quantidade de bibliotecas não irá tornar o Java uma linguagem de linha de execução segura.
Comprovadamente, uma das propriedades mais importantes reforçadas pelas linguagens é a segurança de memória através da coleta automatizada de lixo. Como observado por Dan Grossman em um artigo brilhante (pdf), a segurança na linha de execução através da memória transacional de software é outro tipo de propriedade. Uma evita os protocolos de alocação de memória “pela honra” em todo o programa, e a outra evita bloquear protocolos “pela honra” em todo o programa.
Quando eu apresentei o AmbientTalk no Emerging Languages Camp em 2010, me perguntaram “por que criar uma nova linguagem para isso?”. “Isso” sendo uma programação baseada em simultaneidade e distribuição: o AmbientTalk tem um modelo de simultaneidade e distribuição baseado na comunicação de eventos repetitivos (um formulário de passar mensagens simultaneamente, baseada em atores).
Minha resposta foi uma variação da visão da linguagem que aplica a lei explicitada acima: a de que a linguagem é capaz de reforçar o fato de que não existem raças de níveis inferiores nem impasses, e que toda comunicação de rede é assíncrona para esconder a latência de rede e aumentar a capacidade de resposta, tudo através do seu design. Depois, eu mencionei que o AmbientTalk interopera com o Java: os objetos do AmbientTalk conseguem enviar mensagens para objetos do Java e vice-versa. Nesse ponto, alguém perguntou se isso não violava minhas queridas propriedades de simultaneidade. Respondi que, como o AmbientTalk é uma linguagem, podemos mediar entre os mundos do AmbientTalk e do Java, e iremos automaticamente “amarrar” os objetos AmbientTalk em proxies de linhas de execução seguras, antes de passá-los para objetos Java, até em caso de interoperação entre AmbientTalk e Java, as linhas Java não irão gerar devastação em eventos repetitivos do AmbientTalk. Nesse ponto, a plateia teve um momento “ah!” e compreendeu vantagem da abordagem da linguagem.
Eu escolhi o termo “aplica a lei” porque queria evocar a conotação com as leis da física. Vivemos em um universo que é governado por leis inquebráveis da física. Isso tem desvantagens (não conseguimos viajar mais rápido do que a velocidade da luz), mas também tem vantagens (conseguimos prever a trajetória de um míssil). Assim, um interpretador é uma ferramenta para a construção de um novo universo, com suas inescapáveis “leis da física” para a linguagem que ele interpreta. (Tive essa idéia de um dos trabalhos de sistemas abertos Agorics de K. Eric Drexler e Mark S). Isso tem suas desvantagens (não consigo espiar a memória no Scheme ou no Python), mas também suas vantagens (no Erlang, eu consigo “prever” que um processo é livre de raças de dados de baixo nível, porque ele é, por definição, isolado de outros processos simultâneos).
A ideia de ver uma linguagem como um pequeno universo com suas próprias leis inquebráveis é muito poderosa. Como cientistas da computação, aprendemos que todas as linguagens “nascem iguais”. No início da história do nosso campo, Alonzo Church e Alan Turing suspeitaram que todas as linguagens de certa complexidade são capazes de computar todas as funções matemáticas computáveis. No entanto, na maior parte do tempo, não usamos linguagens de programação para calcular funções matemáticas puras, mas sim para processar informação e interagir com o mundo físico. E esse é o ponto chave: se uma linguagem de programação não tem nenhum objetivo de exibir algo na tela, nenhum programa escrito naquela linguagem será capaz de influenciar a tela do usuário. Um programa poderia construir a representação da tela do usuário na memória, e manipular aquela representação, mas ele nunca será capaz de influenciar a tela “real”. Parece um exemplo trivial, mas ele ilustra a deia de que a linguagem realmente define seu próprio universo, e que um designer de linguagem tem o poder de controlar o que pode ou não acontecer nesse universo.
Linguagens de programação com capacidade de segurança ilustram bem essa ideia. O objetivo dessas linguagens é permitir programas mutuamente suspeitos a cooperar com possivelmente o mesmo espaço de endereço, ao restringir as ações que esses programas podem executar. Nessas linguagens, a idéia apresentada no parágrafo anterior é colocada em prática. Primeiro, a habilidade de interagir com o mundo exterior é parcelada em várias capacidades pequenas, como a habilidade de ler um arquivo específico, de escutar uma conexão socket específica, ou de interagir com um objeto particular da aplicação. Os programas geralmente nascem com um conjunto pequeno, ou nenhum, de tais capacidades, e existem leis sobre como um programa pode adquirir novas capacidades. A segurança fornecida por essas linguagens vêm do fato de que um programa é capaz de interagir com seu ambiente somente se as capacidades especificamente o permitem fazer isso.
Um subproduto da teoria da linguagem que aplica a lei é que as propriedades reforçadas muitas vezes facilitam a compreensão dos programas escritos naquela linguagem, e como resultado isso gera mais propriedades interessantes a partir dela. Sistemas de tipo tradicionais aplicam a lei. O que eu acho entendiante sobre sistemas de tipo tradicionais é que eles fornecem somente garantias bobas (especialmente quando a conversão de texto é permitida).
Eu gostaria de ter um sistema que pudesse me ajudar a caracterizar se meus dados são (de forma provisória) imutáveis, se são acessados por uma linha de execução segura, se a função é livre de efeitos colaterais, determinista ou idempotente, se um operador binário é comutativo ou associativo etc. Quero uma linguagem que garante que minha análise de caso está completa, da maneira que o Subtext faz. Existe uma grande quantidade de propriedades de programas interessantes, de alto nível, e muitas vezes não-locais que não são capazes de ser aproveitadas pelas (principais) linguagens de hoje. Muitas são fundamentalmente impossíveis de derivar, mas tenho certeza de que ainda existem muitas propriedades úteis que poderiam ser melhor integradas nas nossas linguagens de programação.
Usar uma linguagem que aplica a lei é como talvez criar tabus em uma linguagem humana. Ao abolir palavras ou conceitos de uma linguagem, dificultamos ou impossibilitamos falar sobre eles. A Newspeak (a linguagem fictícia, não a linguagem de programação) é um exemplo.
Conclusão
Desde o nascimento da ciência da computação, as linguagens de programação floresceram. Apesar dos insights de Church e Turing de que todas elas nascem iguais, a novas linguagens de programação continuam sendo inventadas. Este artigo tentou fornecer algum insight sobre por que isso acontece, explicitando alguns dos objetivos por trás do processo de design da linguagem.
Nota final: mesmo não sendo um expert em sistemas operacionais, tenho certeza de que muitos (ou todos) dos objetivos que tratei neste artigo encontram ressonância nos sistemas operacionais. Se nós, designers de linguagem, olhássemos na direção da comunidade de sistemas operacionais, tenho certeza de que iríamos descobrir ali os equivalentes do nosso design de linguagens de programação.
?
Texto original disponível em http://soft.vub.ac.be/~tvcutsem/whypls.html