UP | HOME

Escrevendo programas

Table of Contents

Agora que falamos de listas, podemos finalmente falar dos programas de Majestic Lisp.

Programas de Majestic Lisp são tratados como um conjunto de expressões. Essas expressões podem ser símbolos individuais, ou listas com arranjo específico.

Como anteriormente citado, um símbolo individual normalmente é tratado como um rótulo para um valor. Já uma lista pode estar relacionada à aplicação de uma função, execução de uma forma especial ou aplicação de um macro.

Por exemplo, a expressão a seguir define uma função capaz de dobrar um número qualquer, aqui chamada double:

(defn double (x)
  (* x 2))
double

Apesar de ser um uso específico do macro1 defn, a expressão acima continua sendo uma lista, segundo o que já foi dito a respeito da sintaxe das mesmas. A diferença crucial está em não realizarmos quoting na expressão, o que indica que ela deverá ser interpretada.

Durante a interpretação, o interpretador verifica o primeiro símbolo da lista e percebe que defn está atrelado a um macro; subsequentemente, realiza os passos de interpretação necessários que levam, ao final, à atribuição de uma função ao símbolo double.

Veremos a seguir algumas funções e formas especiais que são padrão em Majestic, bem como o uso das mesmas.

1. Declaração de variáveis

A maioria das linguagens possui uma forma de atribuir rótulos a certos valores, e Majestic Lisp não é diferente nesse aspecto. Nela, temos uma forma de definir valores que estarão acessíveis para todo o programa, e também valores que estarão acessíveis apenas em regiões específicas, que geralmente chamamos de escopo.

Outra forma de utilização de uma variável está em redefini-la dinamicamente de acordo com um contexto, porém não abordaremos este método nas seções subsequentes.

1.1. Variáveis globais

Variáveis globais são variáveis que, normalmente, são acessíveis em qualquer região de um programa.

Majestic Lisp armazena, em um estado global, uma tabela que liga certos rótulos (símbolos) a certos valores.

Podemos usar a forma especial def para definir uma variável.

A seguir, usamos def para definir uma variável global x, de valor numérico 5. Como resposta, o interpretador retorna o mesmo símbolo x, indicando que este foi definido com sucesso.

(def x 5)
x

Se, logo em seguida, formos ao REPL e digitarmos o símbolo x para que seja interpretado, o interpretador retornará o valor associado a esse símbolo:

x
5

Caso digitássemos um símbolo não-quotado que não correspondesse a uma variável anteriormente definida, o interpretador informaria que há um erro.

Podemos redefinir o valor de uma variável com facilidade, usando a forma especial set. Ela nos permite realizar atribuições de novos valores a variáveis já existentes.

(set x 9)
x
x
9

set só pode ser utilizado em uma variável global após a mesma ter sido atribuída, através de def.

1.2. Variáveis locais

Há uma forma de definir variáveis apenas temporariamente através da criação de um escopo. O interpretador de Majestic Lisp tratará essa variável como definida apenas dentro do mesmo; fora, a variável já não estará definida.

(let ((y 9))
  y)
9

Na situação acima, usamos o macro let para definir localmente uma única variável y, que estará definida apenas para o corpo desse macro.

O macro let tem a forma a seguir:

(let (<definições>)
  <corpo>)

…onde <definições> é um número arbitrário de duplas na forma (rótulo valor), e <corpo> é uma única expressão, que será interpretada levando em consideração as variáveis localmente definidas.

O exemplo anterior funciona como se tivéssemos definido a variável y globalmente usando o valor 9 e pedíssemos o valor associado a y. Logo em seguida, o interpretador "esquece" que essa variável foi algum dia definida.

Também podemos redefinir o valor de uma variável local, usando a forma especial set:

(let ((y 9))
  (set y 10)
  y)
10

Sombreando variáveis.

Podemos usar definições locais para utilizarmos a ideia de sombreamento, para efetuar redefinições temporárias de variáveis globais, ou até mesmo de variáveis locais, em caso de let's aninhados.

(def x 5)
x
(let ((x 6))
  x)
6
x
5

O exemplo acima mostra uma variável global x cujo valor atribuído é 5; em seguida, criamos um escopo onde x possui o valor 6, e requisitamos nele o valor de x. Após a execução desse escopo, x "volta" a ter o valor associado 5.

2. Funções

Até agora, vimos estruturas especiais de listas a serem interpretadas, mais especificamente formas especiais e macros. Mas e quanto aos outros tipos de lista?

Quando temos uma lista "comum", o interpretador de Majestic Lisp espera que o primeiro elemento da lista seja uma função. Funções são fragmentos de código que realizam operações específicas, e que podem ser reutilizados posteriormente.

Em Majestic Lisp, temos dois tipos de funções: primitivas e de usuário.

2.1. Funções primitivas

As funções primitivas de Majestic Lisp são funções já definidas antes da execução do interpretador. Isso significa que qualquer implementação da linguagem precisa garantir que essas funções possam ser utilizadas.

Uma dessas funções é a função +, responsável principalmente por somar números. Essa função aceita qualquer quantidade de números como argumentos e, quando fornecidos dois ou mais argumentos, ela retorna a soma de todos esses números2.

(+ 2 3)
5
(+ 5 7 1)
13

Há também outras funções primitivas de aritmética, capazes de executar multiplicação (*), divisão (/) e subtração (-). Podemos inclusive usar sublistas para realizar mais operações com o resultado de outras operações; nesse caso, também é pertinente indentar nosso código para melhor legibilidade.

(+ (/ 8 2)
   (+ 1 3)
   (* 7 7))
57

2.2. Aplicação de funções

Outro exemplo de função primitiva, muito utilizada, é cons. Essa função requer exatamente dois argumentos, e pode ser utilizada para criar uma célula cons. Veja o exemplo um pouco mais complexo a seguir.

(let ((x 2)
      (y 3))
  (cons x y))
(2 . 3)

Aqui, temos as variáveis locais x e y, de valores atribuídos 2 e 3, respectivamente.

Ao passarmos as variáveis locais x e y para cons, veja que não estamos quotando esses rótulos. Isso significa que o que está sendo passado a cons não são os nomes das variáveis, mas sim os valores a eles atribuídos.

Da mesma forma, cons é apenas o rótulo de uma certa operação, que cria uma célula cons a partir de dois valores.

O resultado da operação é uma célula cujo car é 2 e cujo cdr é 3. Ao final do escopo do let, as variáveis x e y deixarão de "existir", permanecendo apenas a célula cons retornada, contendo esses valores.

Podemos constatar os valores de car e cdr de uma função através das funções primitivas homônimas, car e cdr. A seguir, temos um exemplo um pouco mais complexo, que toma o retorno do escopo anteriormente demonstrado, colocando-o em uma variável para ser consultada depois.

(def my-cell
  (let ((x 2)
        (y 3))
    (cons x y)))
my-cell
my-cell
(2 . 3)
(car my-cell)
2
(cdr my-cell)
3

2.3. Funções do usuário

As funções de usuário de Majestic Lisp são funções declaradas pelo usuário do sistema, diferentemente das funções primitivas que já existem quando o sistema é iniciado.

A sintaxe para a declaração de uma função envolve a forma especial fn, que pode ser compreendida como:

(fn <argumentos> <corpo>)

…onde <argumentos> é, normalmente, uma lista de argumentos para a função, e <corpo> é uma única expressão a ser executada durante a invocação da função.

O exemplo a seguir envolve a função que eleva um número ao quadrado. Como podemos observar, a função recebe um único argumento (aqui chamado x), e então multiplica-o por si mesmo.

O resultado retornado é um formato de impressão de uma função, que normalmente não pode ser reinserido no REPL.

(fn (x) (* x x))
#<function (fn (x)) {0x565242ffe170}>

Funções são cidadãos de primeira-classe em Majestic Lisp. Isso significa que elas podem ser rotuladas, repassadas em aplicações de outras funções e atribuídas a novos rótulos, assim como faríamos com qualquer outro valor.

Podemos, por exemplo, atribuir a nossa função ao símbolo square:

(def square
  (fn (x) (* x x)))
square

Para que não precisemos usar com frequência a dupla deffn, instituiremos um atalho sintático (um macro) que faça este trabalho por nós, tornando as funções mais simples de serem lidas. Este macro se chamará defn, e poderá ser escrito usando a regra sintática:

(defn <nome> <argumentos> <corpo>)

Se esse "atalho sintático" fosse expandido, seria exatamente igual ao exemplo anterior, em que usamos def e fn, portanto as regras para o símbolo, os argumentos e o corpo ainda se aplicam.

(defn square (x)
  (* x x))
square

A aplicação de uma função do usuário é muito similar, se não idêntica, à aplicação de uma função primitiva.

(square 5)
25

Funções locais.

Assim como no caso das variáveis locais, existem situações onde é interessante criar funções de usuário que só valham para certos escopos. Isso pode ser feito usando a forma letfn, que atribui uma função a um símbolo em um escopo léxico, tal qual a forma let.

Similar a defn, cada cláusula de letfn, de forma análoga a let, define uma função local. A sintaxe de cada cláusula é idêntica à de defn, excluindo-se o uso do símbolo defn em si.

O exemplo a seguir define globalmente a função de usuário 1+, que soma uma unidade a um certo número. Em seguida, criamos um escopo onde 1+ é redefinido como uma função que soma um número a si mesmo; tal definição desaparece ao fim desse escopo.

(defn 1+ (x) (+ x 1))
1+
(letfn ((1+ (x)
          (+ x x)))
  (1+ 5))
10
(1+ 5)
6

Funções globais recursivas.

É interessante observarmos como funciona a recursão em funções definidas globalmente em Majestic Lisp.

A função foo a seguir é recursiva (pois sua definição depende da invocação de si mesma).

(defn foo (n)
  (when (< n 3)
    (print "foo #{}" (1+ n))
    (foo (1+ n))))
foo
(foo 0)
foo #1
foo #2
foo #3
nil

Durante a execução de foo, um interpretador de Majestic Lisp consultará a tabela local de símbolos para realizar uma nova aplicação da função atrelada ao símbolo foo. Isso ocorre sem maiores problemas, já que foo está registrado em um contexto visível em toda a aplicação.

Funções locais recursivas.

O uso de letfn é suficiente para a maioria das situações onde funções locais são necessárias, porém, este exemplo falha quando a função local precisa ser recursiva e precisa ser executada em outro escopo que não seja o local.

Considere o exemplo a seguir. Temos um uso de letfn que define uma função local foo, igualmente recursiva como no exemplo de recursão em funções globais. Todavia, o escopo de letfn retorna a função ligada a foo localmente, que é então atribuída ao símbolo bar globalmente.

(def bar
  (letfn ((foo (n)
            (when (< n 3)
              (print "foo #{}" (1+ n))
              (foo (1+ n)))))
    foo))
bar

Quando bar for executada, note que, mesmo que a função tenha sido originalmente atribuída como foo, como este símbolo já não existe no contexto local (e nem em um contexto global), bar executará apenas uma vez, mostrando um erro assim que foo for invocada.

(bar 0)
foo #1
Error: foo is unbound

Isso ocorre porque, quando definimos uma função recursiva local via letfn, essa função não captura o escopo na qual foi criada; em outras palavras, quando a execução de foo ocorre fora do letfn, foo é incapaz de encontrar uma referência a si mesmo3.

Para tanto, podemos usar a forma especial letrec. Esta forma especial funciona de forma idêntica a letfn, porém garante que cada função local definida tenha acesso permanente ao escopo em que todas as funções de letrec foram definidas.

letrec pode, então, ser utilizado não apenas em contextos de definição local de funções recursivas que sairão daquele escopo, como também pode ser utilizado para definir funções locais que se utilizam mutuamente, onde uma ou mais delas também sairá do escopo de letrec.

(def bar
  (letrec ((foo (n)
             (when (< n 3)
               (print "foo #{}" (1+ n))
               (foo (1+ n)))))
    foo))
bar

A definição de bar, dessa vez, carrega uma função que, antes, estava associada ao símbolo foo; essa função, porém, captura o contexto de letrec, onde o símbolo foo está bem-definido como sendo uma referência a ela mesma.

(bar 0)
foo #1
foo #2
foo #3
nil

3. Controle de fluxo

Majestic Lisp também provê algumas estruturas capazes de controlar o fluxo de execução de programas. A linguagem segue a estrutura de outros Lisps nesse sentido, replicando com muita similaridade algumas formas especiais canônicas.

O primeiro tipo de condicional envolve a forma especial if. Essa forma é escrita segundo a regra sintática:

(if <condição> <consequência> <alternativa>)

Assim, <condição>, <consequência> e <alternativa> são expressões a serem interpretadas segundo certas regras especiais.

Primeiramente, Majestic Lisp tentará interpretar a expressão <condição>. Caso seu valor seja verdadeiro (ou seja, diferente de nil), a expressão <consequência> será interpretada. Caso contrário, a expressão <alternativa> será interpretada.

A função with-one a seguir verifica pela nulidade de um objeto. Caso esse objeto seja nulo, será retornado um cons entre 0 e 1; caso contrário, será retornado um cons entre o objeto em questão e 1.

(defn with-one (x)
  (if (nilp x)
      (cons 0 1)
      (cons x 1)))
with-one
(with-one nil)
(0 . 1)
(with-one 5)
(5 . 1)

Outro tipo de condicional é a forma cond. Essa forma é extremamente útil quando lidamos com condicionais que envolvem mais que apenas um predicado.

Por exemplo, uma condicional como esta:

(if pred1
    conseq1
    (if pred2
        conseq2
        conseq3))

…pode ser reescrita, usando cond, da seguinte forma:

(cond (pred1 conseq1)
      (pred2 conseq2)
      (t conseq3))

Assim, podemos transformar o que seria um encadeamento sucessivo de if's em algo sintaticamente agradável.

A função abs retratada a seguir é uma forma redundante de definir uma função que calcula o valor absoluto de um número. Caso o número seja igual a 0, será retornado 0; caso o número seja maior que 0, este mesmo número será retornado; mas, se o número for menor que 0, então será retornado o valor aritmeticamente oposto a ele.

(defn abs (x)
  (cond ((zerop x) 0)
        ((> x 0) x)
        (t (- x))))
abs
(abs 0)
0
(abs 5)
5
(abs -5)
5

4. Quasiquoting

Um dos elementos cruciais da metaprogramação em Majestic Lisp é a ideia de quasiquoting. O conceito é muito similar à ideia de quoting, onde toda uma lista quasiquotada é, por padrão, tratada como dados pelo interpretador.

Todavia, empregar o recurso do quasiquote permite ao programador estipular alguns dados no interior de uma expressão que serão efetivamente interpretados. Em outras palavras, alguns elementos da lista serão dados produzidos durante o processo de retorno da mesma, enquanto outros permanecerão intactos.

Uma lista quasiquotada é precedida pelo acento grave (`), ao invés do apóstrofo que simboliza o quote ('). Algumas formas dentro dessa lista poderão estar precedidas pelos símbolos especiais de unquote (,) e unquote-splice (,@).

(defn hello (name)
  `(good morning ,name and have a nice day))
hello
(hello 'fulano)
(good morning fulano and have a nice day)

A função de usuário hello apresentada acima recebe um único argumento – name – que será inserido diretamente numa lista, precedido pelos respectivos símbolos good e morning. Veja que, com o quasiquote, é possível deixar claro o local exato onde o valor associado a name ficará, e este valor é marcado através do unquote da expressão name nesta lista.

Todavia, o processo de unquote não realiza distinção se o valor associado a name é uma lista ou não:

(hello '(fulano da silva))
(good morning (fulano da silva) and have a nice day)

Em uma situação ideal, podemos realizar essa distinção reescrevendo a função hello de forma que, quando o valor recebido na chamada da função for uma lista, apenas seu conteúdo seja adicionado ao resultado. A este processo, damos o nome de unquote-splice.

(defn hello (name)
  (if (consp name)
      `(good morning ,@name and have a nice day)
      `(good morning ,name and have a nice day)))
hello
(hello '(fulano da silva))
(good morning fulano da silva and have a nice day)
(hello 'fulano)
(good morning fulano and have a nice day)

Veja que, como previsto, apenas o conteúdo da lista quotada (fulano da silva) foi adicionado diretamente à lista, como se seus elementos tivessem sido "desempacotados" na lista de nível superior.

Footnotes:

1

Macros auxiliam o programador, possibilitando a definição de nova sintaxe que, em tempo de execução, se transforma em sintaxe menos intuitiva. Veremos como isso funciona posteriormente.

2

É interessante notar que, em Majestic Lisp, a função + também pode operar sobre um único número, calculando seu conjugado – uma operação que funciona com números complexos. Caso o número não possua esse subtipo, + apenas retorna o número sem modificações.

3

Isso ocorre por intermédio de escopo dinâmico em letfn, o que pode inclusive não ser desejável.

Author: Lucas S. Vieira

Created: 2022-10-24 seg 00:27

Validate