Scheme, uma breve introdução (Parte 1)

Considerado até hoje uma lição de minimalismo no design de linguagens de programação, o Scheme foi concebido no MIT durante os anos 70 por Guy Steele e Gerald Sussman. Trata-se de uma linguagem concisa, elegante e bastante fácil - a sintaxe inteira pode ser aprendida em questão de meia hora, ou talvez menos - sendo assim interessante para se introduzir conceitos de programação, como é feito no clássica obra “Structure and Interpretation of Computer Programs”, e é sobre ele de que vamos falar em uma série de artigos, começando por este.

Padrões e Implementações

O Scheme, assim como várias linguagens populares, não consiste em um único interpretador ou compilador, e sim num conjunto de especificações técnicas, tal qual o Javascript com o ECMAScript e o C com o ANSI C, cada uma com suas respectivas implementações. Atualmente, existem três principais especificações do Scheme: a R5RS, contando o maior número de implementações e considerada um das mais simples; a R6RS, que introduziu um grande número de adições à linguagem, mas enfrentou críticas por divagar do minimalismo; e por fim o R7RS, o mais recente, que tenta atingir um equilíbrio entre minimalismo e pragmatismo ao dividir a linguagem em duas: uma versão menor, com menos recursos, e outra maior para projetos reais.

Aqui vamos nos restringir ao R5RS, mas primeiramente focando nos aspectos comuns a todas as outras specs. A implementação que eu particulamente recomendo é a do Racket, antigo PLT Scheme. Tem suporte completo ao R5RS e é fácil de se instalar nas principais plataformas, além de vir com uma IDE pronta - o DrRacket.

Algumas outras implementações boas são o Chicken Scheme, que pode ser compilado para C; o GNU Guile, desenvolvida para ser embutida em outros programas; e o Chez Scheme, além do clássico MIT Scheme.

Primeiros Passos

Comecemos com um simples “Hello World”. Caso você esteja usando o DrRacket, vai ter de inserir #lang scheme na primeira linha dos arquivos, e recarregar a REPL.

(display "Hello World")

O Scheme é uma linguagem da família dos Lisps; isso significa que toda expressão vai seguir a formato (<função/operador> <parâmetros>). Essa regra vale inclusive para operadores aritméticos (note que os pontos e vírgulas representam comentários, trechos ignorados):

(+ 2 2) ; Adicione dois a dois
(* 2 2) ; Multiplique dois por dois
(- 4 2) ; Subtraia 2 de 4 

Uma vantagem dessa abordagem é que não fazemos distinção entre função e operadores; a sintaxe é a mesma. Seguindo essa notação, compor expressões também é simples:

(display (* (+ 2 2)
            (- 6 2)))

Com algumas exceções específicas, primeiramente são computados os parâmetros para depois computar a expressão em si:

; Antes de computar essa expressão
(display (* (+ 2 2) (- 6 2)))

; O interpretador precisa antes conhecer o valor de
(* (+ 2 2) (- 6 2))

; que por sua vez, depende do valor de
(+ 2 2)
; e
(- 6 2)

Toda expressão iniciada em função segue essa regra.

Símbolos e Valores

Uma capacidade comum a todas as linguagens de programação modernas é a capacidade de associar nomes a valores, ou seja, de criar variáveis. Aqui associamos o valor 3.14 ao símbolo pi, e em seguida o usamos em um cálculo de perímetro de um círculo de raio 10.

(define pi 3.14)
(* 2 pi 10) 

; Uma vez que retornam valores, o define aceita expressões
(define d (* 2 10))

O define marca uma das exceções ao modelo mental de interpretação introduzido anteriormente, pois o Scheme não tenta “tirar” nenhum valor de pi, como faria em outras circunstâncias; ele apenas entende que se trata de um símbolo, e que deve ser associado a um outro valor. Verifica-se logo na próxima expressão que já se passa a buscar o valor associado ao símbolo, em vez de ignorá-lo. A noção de símbolo será aprofundada mais à frente.

Existem outras maneiras de se criar variáveis, como o let:

(let ((pi 3.14))
  (* 2 pi 10))

Assim como o define, o let constitui uma exceção ao nosso modelo. Sua sintaxe permite realizar várias associações de uma só vez:

(let ((pi 3.14) 
      (d (* 2 10)))
  (* pi d))

No entanto, a maior diferença entre os dois gira em torno de contexto: enquanto o define modifica o contexto em que foi introduzido, o let cria um novo contexto, em que as variáveis inseridas só podem ser acessadas dentro ele. Enquanto um afirma “se antes pi é indefinido, agora passa a valer 3.14”, o outro avisa “aqui pi vale 3.14”.

Lambda

Assim como temos as operações aritméticas básicas representadas por suas devidas funções, queremos também expressar nossos própios procedimentos, criar nossas própias funções; expressar, por exemplo, o conceito de elevar-se um número ao quadrado. Conseguimos isso criando uma nova função, através do lambda, e associando-a a um símbolo.

(define quadrado (lambda (x) (* x x)))
(quadrado 3) ; Eleve três ao quadrado

Como em outras linguagens de programação funcionais, as funções são apenas outro tipo de valor, como strings, caracteres, e números, e não precisam estar sempre associadas a um nome, como ocorre em linguagens como o C. Assim como podemos representar texto com strings, “isto é um texto”, podemos expressar o procedimento de elevar-se uma incógnita ao quadrado usando o lambda, (lambda (x) (* x x)), e depois associar um desses dois a um nome. Em quase todos os casos usamos, no entanto, a seguinte abreviação:

(define (quadrado x) (* x x))
(quadrado 5)

O termo “lambda” é uma referência ao Cálculo Lambda, um modelo matemático na Teoria da Computação criado por Alonzo Churchill, o qual exerce grande influência nas linguagens funcionais.

E por hora isso é tudo. No próximo artigo falarei sobre condições e recursão - conceito imprescidível pra quem se interessa por linguagens funcionais. Espero que tenha gostado, e até.