0%
Pular para o conteúdo principal
0%

4 Abstração Metalinguística

...É nas palavras que está a magia — Abracadabra, Abre-te Sésamo, e o resto — mas as palavras mágicas em uma história não são mágicas na próxima. A verdadeira magia é entender quais palavras funcionam, e quando, e para quê; o truque é aprender o truque.

...E essas palavras são feitas das letras do nosso alfabeto: algumas dúzias de rabiscos que podemos desenhar com a caneta. Esta é a chave! E o tesouro também, se ao menos pudermos colocá-lo em nossas mãos! É como se — como se a chave para o tesouro fosse o tesouro!

— John Barth, Chimera

Em nosso estudo de design de programas, vimos que programadores experientes controlam a complexidade de seus designs com as mesmas técnicas gerais usadas por designers de todos os sistemas complexos. Eles combinam elementos primitivos para formar objetos compostos, abstraem objetos compostos para formar blocos de construção de nível superior e preservam a modularidade adotando visões apropriadas de larga escala da estrutura do sistema. Ao ilustrar essas técnicas, usamos JavaScript como uma linguagem para descrever processos e para construir objetos e processos de dados computacionais para modelar fenômenos complexos no mundo real. No entanto, à medida que confrontamos problemas cada vez mais complexos, descobriremos que JavaScript, ou de fato qualquer linguagem de programação fixa, não é suficiente para nossas necessidades. Devemos constantemente recorrer a novas linguagens para expressar nossas ideias de forma mais eficaz. Estabelecer novas linguagens é uma estratégia poderosa para controlar a complexidade no design de engenharia; muitas vezes podemos aprimorar nossa capacidade de lidar com um problema complexo adotando uma nova linguagem que nos permita descrever (e, portanto, pensar sobre) o problema de uma maneira diferente, usando primitivos, meios de combinação e meios de abstração que são particularmente adequados para o problema em questão.1

A programação é dotada de uma infinidade de linguagens. Existem linguagens físicas, como as linguagens de máquina para computadores específicos. Essas linguagens se preocupam com a representação de dados e controle em termos de bits individuais de armazenamento e instruções primitivas de máquina. O programador de linguagem de máquina se preocupa em usar o hardware disponível para construir sistemas e utilitários para a implementação eficiente de computações com recursos limitados. Linguagens de alto nível, construídas sobre um substrato de linguagem de máquina, ocultam preocupações sobre a representação de dados como coleções de bits e a representação de programas como sequências de instruções primitivas. Essas linguagens têm meios de combinação e abstração, como declaração de função, que são apropriados para a organização em maior escala de sistemas.

Abstração metalinguística — estabelecer novas linguagens — desempenha um papel importante em todos os ramos do design de engenharia. É particularmente importante para a programação de computadores, porque na programação não só podemos formular novas linguagens, mas também podemos implementar essas linguagens construindo avaliadores. Um avaliador (ou interpretador) para uma linguagem de programação é uma função que, quando aplicada a uma declaração ou expressão da linguagem, executa as ações necessárias para avaliar essa declaração ou expressão. Não é exagero considerar isso como a ideia mais fundamental em programação:

O avaliador, que determina o significado de declarações e expressões em uma linguagem de programação, é apenas outro programa.

Apreciar este ponto é mudar nossas imagens de nós mesmos como programadores. Passamos a nos ver como designers de linguagens, em vez de apenas usuários de linguagens projetadas por outros.

Na verdade, podemos considerar quase qualquer programa como o avaliador de alguma linguagem. Por exemplo, o sistema de manipulação de polinômios da seção 2.5.3 incorpora as regras da aritmética polinomial e as implementa em termos de operações em dados estruturados em lista. Se aumentarmos este sistema com funções para ler e imprimir expressões polinomiais, teremos o núcleo de uma linguagem de propósito especial para lidar com problemas em matemática simbólica. O simulador de lógica digital da seção 3.3.4 e o propagador de restrições da seção 3.3.5 são linguagens legítimas por direito próprio, cada uma com seus próprios primitivos, meios de combinação e meios de abstração. Vista desta perspectiva, a tecnologia para lidar com sistemas de computador em larga escala se funde com a tecnologia para construir novas linguagens de computador, e a ciência da computação em si se torna nada mais (e nada menos) do que a disciplina de construir linguagens descritivas apropriadas.

Embarcamos agora em um tour pela tecnologia pela qual as linguagens são estabelecidas em termos de outras linguagens. Neste capítulo, usaremos JavaScript como base, implementando avaliadores como funções JavaScript. Daremos o primeiro passo para entender como as linguagens são implementadas construindo um avaliador para o próprio JavaScript. A linguagem implementada por nosso avaliador será um subconjunto de JavaScript. Embora o avaliador descrito neste capítulo seja escrito para um subconjunto específico de JavaScript, ele contém a estrutura essencial de um avaliador para qualquer linguagem projetada para escrever programas para uma máquina sequencial. (De fato, a maioria dos processadores de linguagem contém, bem no fundo deles, um pequeno avaliador.) O avaliador foi simplificado para fins de ilustração e discussão, e alguns recursos foram deixados de fora que seriam importantes para incluir em um sistema JavaScript de qualidade de produção. No entanto, este avaliador simples é adequado para executar a maioria dos programas deste livro.2

Uma vantagem importante de tornar o avaliador acessível como um programa JavaScript é que podemos implementar regras de avaliação alternativas descrevendo-as como modificações no programa avaliador. Um lugar onde podemos usar esse poder com bom efeito é para obter controle extra sobre as maneiras pelas quais os modelos computacionais incorporam a noção de tempo, que foi tão central para a discussão no capítulo 3. Lá, mitigamos algumas das complexidades de estado e atribuição usando streams para desacoplar a representação do tempo no mundo do tempo no computador. Nossos programas de stream, no entanto, às vezes eram desajeitados, porque eram restringidos pela avaliação em ordem aplicativa de JavaScript. Na seção 4.2, mudaremos a linguagem subjacente para fornecer uma abordagem mais elegante, modificando o avaliador para fornecer avaliação em ordem normal.

A seção 4.3 implementa uma mudança linguística mais ambiciosa, pela qual declarações e expressões têm muitos valores, em vez de apenas um único valor. Nesta linguagem de computação não determinística, é natural expressar processos que geram todos os valores possíveis para declarações e expressões e depois pesquisam aqueles valores que satisfazem certas restrições. Em termos de modelos de computação e tempo, isso é como ter o tempo se ramificando em um conjunto de "futuros possíveis" e depois pesquisar as linhas de tempo apropriadas. Com nosso avaliador não determinístico, acompanhar múltiplos valores e realizar pesquisas são tratados automaticamente pelo mecanismo subjacente da linguagem.

Na seção 4.4, implementamos uma linguagem de programação lógica na qual o conhecimento é expresso em termos de relações, em vez de em termos de computações com entradas e saídas. Embora isso torne a linguagem drasticamente diferente de JavaScript, ou de fato de qualquer linguagem convencional, veremos que o avaliador de programação lógica compartilha a estrutura essencial do avaliador JavaScript.

Footnotes

  1. A mesma ideia é pervasiva em toda a engenharia. Por exemplo, engenheiros elétricos usam muitas linguagens diferentes para descrever circuitos. Duas dessas são a linguagem de redes elétricas e a linguagem de sistemas elétricos. A linguagem de redes enfatiza a modelagem física de dispositivos em termos de elementos elétricos discretos. Os objetos primitivos da linguagem de redes são componentes elétricos primitivos, como resistores, capacitores, indutores e transistores, que são caracterizados em termos de variáveis físicas chamadas tensão e corrente. Ao descrever circuitos na linguagem de redes, o engenheiro se preocupa com as características físicas de um design. Em contraste, os objetos primitivos da linguagem de sistemas são módulos de processamento de sinais, como filtros e amplificadores. Apenas o comportamento funcional dos módulos é relevante, e os sinais são manipulados sem preocupação com sua realização física como tensões e correntes. A linguagem de sistemas é construída sobre a linguagem de redes, no sentido de que os elementos dos sistemas de processamento de sinais são construídos a partir de redes elétricas. Aqui, no entanto, as preocupações são com a organização em larga escala de dispositivos elétricos para resolver um determinado problema de aplicação; a viabilidade física das partes é assumida. Esta coleção estratificada de linguagens é outro exemplo da técnica de design estratificado ilustrada pela linguagem de imagens da seção 2.2.4.

  2. Os recursos mais importantes que nosso avaliador deixa de fora são mecanismos para lidar com erros e suportar depuração. Para uma discussão mais extensa de avaliadores, veja Friedman, Wand e Haynes 1992, que fornece uma exposição de linguagens de programação que prossegue através de uma sequência de avaliadores escritos no dialeto Scheme de Lisp.