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.
Epílogo
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é.