Sparse ThoughtsComeçando com Clojure

Há mais ou menos dois anos eu resolvi me aprofundar no estudo de linguagens de programação funcionais. Entre as candidatas estavam Scala, Erlang, Haskel e Clojure.

Clojure estava em último lugar na minha lista de interesses devido à experiências anteriores não muito agradáveis com outros Lisps, mas depois de ler uma citação do Richard Stallman achei que deveria tentar de novo (não que eu costume concordar com ele, mas fiquei curioso):

The most powerful programming language is Lisp. If you don’t know Lisp (or its variant, Scheme), you don’t know what it means for a programming language to be powerful and elegant. Once you learn Lisp, you will understand what is lacking in most other languages.

O que é Clojure?

Clojure é um Lisp (eu sei que já deu pra entender isso) e compartilha características comuns à maioria deles, ou seja, Clojure é uma linguagem:

Além de ter suas características próprias:

Nossa, quanta coisa legal! Mas o que significa isso tudo mesmo?

Boa pergunta, curioso leitor. Pretendo respondê-la neste e em outros posts futuros, mas primeiro vamos ver com o que ela se parece.

Primeiro contato

Não poderia fazer um artigo introduzindo uma linguagem de programação sem o clássico hello world, certo? Pois aqui vai ele:

(println "Hello, world")

Nada mal, hein? println é uma função que recebe como parâmetro a string "Hello world". Tirando o posicionamento estranho dos parênteses, parece-se muito com a maioria das linguagens de script por aí.

E é exatamente esta utilização dos parênteses que reflete as origens da linguagem. Lisp vem de LISt Processing, ou Processamento de Listas. Uma lista é uma estrutura de dados similar aos conhecidos Arrays de outras linguagens.

// JavaScript
["Dexter Morgan", "Hannibal Lecter", "Patrick Bateman"]

Troque os colchetes por parênteses e remova as vírgulas — ou não, vírgulas são consideradas whitespace em Clojure — e você terá uma lista.

;; Essas duas listas são equivalentes
'("Dexter Morgan" "Hannibal Lecter" "Patrick Bateman")
'("Dexter Morgan", "Hannibal Lecter", "Patrick Bateman")
;; Os apóstrofos na frente das listas precisam estar lá.
;; Você entenderá o porquê.
;; A propósito, ; serve para iniciar um comentário que se
;; estende até o fim da linha

Mas espera aí? O exemplo do hello world se parece muito com isso, não é? Pergunta o intrigado leitor. Continue assim, leitor. Você está indo bem.

Listas. Listas por toda parte
Listas. Listas por toda parte

Aí está a beleza da coisa. Em Clojure (e em todo Lisp) utilizamos listas para escrever código. O que acontece é que ao avaliar uma lista o primeiro elemento deve ser uma função (ou, como nesse caso, um nome associado a uma função) e os outros elementos serão os argumentos dela. Essa é a causa dos apóstrofos no início das listas de nomes acima. O primeiro elemento, uma string, não pode ser executado como uma função, então precisamos indicar que esta trata-se apenas de uma lista de valores. Mais exemplos:

(+ 40 2) ; => 42
(- 9 4) ; => 5

OK. Isso é estranho!

Sim, à primeira vista é estranho mesmo, mas você se acostuma rápido. A sintaxe dos Lisps é muito simples: todas as operações são feitas utilizando-se funções através da avaliação de listas2. No exemplo acima, o + e o - não são operadores especiais com sintaxe especial, como em outras linguagens. São apenas funções incluídas na biblioteca padrão da linguagem inteligentemente nomeadas para, adivinhe, somar e subtrair, respectivamente.

Essa sintaxe única traz algumas vantagens além da óbvia de ser fácil de memorizar:

Nem tudo é vantagem. Essa sintaxe, em alguns casos, tende a acumular parênteses amontoados no final de grandes expressões, embora existam formas de contornar isso. Também é preciso um pouco de prática até se conseguir ler claramente o código escrito por outras pessoas (pelo menos foi o meu caso).

Sem dúvidas, os parênteses são os principais motivos de programadores iniciantes (eu incluso) torcerem o nariz ao se deparar com Lisp, mas não tema. Qualquer bom editor vai te ajudar com isso.

read-eval-print loop

Antes de continuar a descrever a linguagem, deixa eu explicar como colocar as mãos no compilador e testar os códigos você mesmo.

Você pode obter a versão estável mais recente do compilador aqui. Como eu disse, Clojure utiliza a Java Virtual Machine para executar seus programas. Este zip contém, entre outras coisas, o arquivo clojure-1.7.0.jar, que possui tudo o que é necessário para compilar, executar e testar os seus programas (considerando que você já tem a JVM instalada, caso contrário, vá aqui primeiro).

Para os não-iniciados no universo javistico, um JAR é um pacote pelo qual se distribui um executável ou biblioteca. Algo parecido com as gems do Ruby, eggs do Python ou os crates do Rust. Existem outras formas de obter e gerenciar dependências Java/Clojure (Maven, Leiningem, Boot, etc), mas por enquanto só o download é suficiente.

Num terminal, execute o seguinte comando no mesmo diretório onde está o arquivo clojure-1.7.0.jar que você extraiu do zip baixado:

java -cp clojure-1.7.0.jar clojure.main
Bem-vindo ao REPL
Bem-vindo ao REPL

O prompt que aparece, aguardando que algo seja digitado, é parte do REPL, ou Read-Eval-Print Loop, uma ferramenta distribuída junto com o compilador que permite a execução interativa de código. Ele o comando digitado, avalia e executa as expressões, imprime o resultado e volta a esperar outro comando. REPLs não são exclusividades dos Lisps, mas costumam integrar o workflow dos programadores dessas linguagens mais que os outros. Vamos tentar uma das expressões que já mostrei: digite (+ 1 2 3 4 5) e tecle enter.

Exemplo 1: uma calculadora bem incomum
Exemplo 1: uma calculadora bem incomum

Expressão lida (read), avaliada/executada (eval), resultado impresso (print) e prompt esperando uma nova expressão (loop). Tente usar outras funções matemáticas (+, -, /, *), lógicas (>, <, >=, <=), de entrada de dados (read-line) e de saída (print, println). Para interromper o REPL, pressione Control+C ou Control+D.

Formas especiais

De volta ao conteúdo principal. Agora que já entendemos a ideia de listas serem avaliadas como chamadas de funções, podemos escrever qualquer tipo de programa em Clojure, certo? Bem, nem tanto.

Mais cedo do que você pensa seu programa precisará de mais poderes, como decidir se precisa ou não executar certa função ou repetir uma execução várias vezes. Para tal, existem formas4 especiais que alteram a maneira como o compilador/interpretador avalia as expressões lidas. Não irei descrever todas aqui hoje (não são muitas), só o necessário para começar.

if

(if (= 1 1) "Yea!" "Nay!")
;; = é a função usada para comparar valores, equivalente ao
;; operador == da maioria das outras linguagens

O if é a forma usada para avaliação condicional de expressões. Ele tem a mesma cara das outras funções mostradas até agora, né? Essa é a ideia. É uma lista com 4 elementos, o que poder ser entendido como uma função que recebe 3 argumentos. Porém, diferente de outras listas, nem todos os seus elementos serão avaliados.

Se o resultado da avaliação do primeiro argumento for verdadeiro5 o valor da forma como um todo será o resultado da avaliação do segundo argumento e o terceiro argumento nunca será avaliado. Se o primeiro argumento for algo que indique falsidade ocorrerá o inverso. O interpretador irá ignorar o segundo argumento e avaliar o terceiro. Em Ruby seria algo assim:

if 1 == 1
  "Yea!"
else
  "Nay!"
end

let

A essa altura você já está se perguntando porque não declarei nenhuma variável nos exemplos mostrados. É que tem uma pegadinha aqui. Em Clojure você não atribui valores a variáveis como você pode estar acostumado. Você associa um valor a um nome. Não parece, mas tem uma diferença. Em C, por exemplo, você pode atribuir novos valores a uma variável pré-existente:

#include <stdio.h>

int main() {
  int number = 0;
  printf("%p: %d\n", &number, number);
  number = 9999;
  printf("%p: %d\n", &number, number);
  return 0;
}

// a saída seria algo como:
// 0x7fff5717e648: 0
// 0x7fff5717e648: 9999

Nesse exemplo dá pra ver que a variável number é uma referência sempre à posição da memória 0x7fff5717e648, mesmo depois de mudarmos o valor. Caso duas threads estivessem lendo e/ou alterando essa variável o comportamento seria imprevisível.

Clojure evita esse problema impedindo — ou pelo menos dificultando — que você altere o valor de variáveis. Você cria associações, ou bindings, entre nomes e valores para um escopo. Isso é feito usando a forma especial let.

(let [a 10
      b 20
      c (+ a b)] ;; cada binding tem acesso aos bindings anteriores
  (println (+ a b c)))
;; => 60

Dois (ou mais) argumentos são passados para o let. Um vetor6 de bindings, composto de pares símbolo/valor, e um corpo, que é o conjunto de expressões que forma o escopo no qual os símbolos estarão associados aos valores mencionados. O resultado da última expressão do corpo será o valor do let como um todo.

def

Eu meio que menti quando disse que a única forma de dar nomes à valores é através de let. Você vai precisar dar nomes à dados que viverão por todo o tempo de execução de um programa e não um pequeno escopo. Imagine as funções que você define, por exemplo, ou valores que são considerados constantes.

Para definir esses nomes existe a forma especial def. Nome bem óbvio, né? Ela é usada da seguinte maneira:

(def pi 3.14) ;; um valor muito pouco preciso de 𝛑
;; => #'user/pi [I]
pi
;; => 3.14 [II]
(def three-pi-over-two (/ (* 3 pi) 2))
;; => #'user/three-pi-over-two [III]
three-pi-over-two
;; => 4.71 [IV]

Sem mistério. Uma lista de três elementos: o símbolo def, um símbolo que representa o nome da variável global sendo criada e uma expressão que será avaliada e o seu resultado atribuído à variável. Observe que o def retorna a variável em si (indicado pelos caracteres #'7 em [I] e [III]) e não o seu valor. Observe também que os nomes das variáveis retornadas são iniciados por user/. Esse é o nome do namespace no qual ela foi definida. Namespaces são utilizados para agrupar funções relacionadas e evitar conflitos de nomes de variáveis. Mais sobre namespaces em outra oportunidade.

Depois das definições, quaisquer utilizações dos símbolos usados resulta na avaliação do mesmo como o valor armazenado na variável correspondente, como pode ser visto em [II] e [IV].

fn

Como não poderia deixa de ser numa linguagem funcional, é preciso existir uma forma de definir funções. Isso é trabalho do fn (Clojure tem uma filosofia de que se uma função ou macro será muito utilizada ele deve ter um nome bem curto, por isso, fn e não function, por exemplo).

(fn [name] (str "Hello, " name))
;; => #object[user$eval1$fn__2 0x465232e9 "user$eval1$fn__2@465232e9"]

Ao entrar a expressão acima no REPL ele devolverá algo bem estranho como mostrado acima. Não se preocupe com isso. fn retorna uma função e o REPL tenta imprimi-la. Essa é a representação interna dela e não é muito útil para nós.

Nenhuma novidade na sintaxe. De novo temos uma lista, agora com 3 (ou mais) elementos. O segundo é um vetor de bindings, semelhante ao do let, mas sem os valores. Esses valores só serão informados quando executarmos a função. À partir do terceiro elemento temos o corpo da função. Também como no let, o corpo compõe o escopo onde os bindings estarão acessíveis e o valor da avaliação da última expressão do corpo será o valor de retorno da função. No entanto, esse corpo não é executado de imediado, apenas quando a função for “chamada”.

Só tem um problema, como eu chamo essa função? Mais uma vez surpreende o antenado leitor fazendo a pergunta certa.

Já sabemos que uma função será avaliada quando for o primeiro elemento de uma lista, então podemos fazer isso:

((fn [name] (str "Hello, " name)) "Dexter")
;; => "Hello, Dexter"
((fn [name] (str "Hello, " name)) "Jack")
;; => "Hello, Jack"

Mas isso não é muito prático nem reusável, afinal nas duas execuções duas cópias distintas da função são criadas. Sem falar que seria loucura ficar copiando funções de uma lado pro outro. Podemos fazer melhor. E se você está mesmo acompanhando o post — ou já programou em JavaScript8 — sabe muito bem o que fazer:

(def greet
  (fn [name] (str "Hello, " name)))
;; => #'user/greet
(greet "Dexter")
;; => "Hello, Dexter"
(greet "Jack")
;; => "Hello, Jack"

Lembra da homoiconicidade? Código é dado, e como tal pode ser associado a nomes como qualquer outro tipo. E a necessidade de se atribuir nomes a funções é tão comum que existe uma forma ainda mais prática de se fazer isso:

(defn greet [name]
  (str "Hello, " name))
;; => #'user/greet

Compare com o exemplo anterior. A criação da função, com fn, e a atribuição do nome, com def, podem ser reduzidas para apenas um passo com a macro defn.

Cenas dos próximos capítulos

Se você chegou até aqui, acredito que esteja realmente interessado em Clojure e espero que minhas explicações tenham sido claras o suficiente (se não foram, fique à vontade para pedir esclarecimentos e deixar sugestões nos comentários).

Este post não chegou nem a arranhar a superfície da linguagem, porém já ficou grande o bastante para darmos por encerrado o dia de trabalho. Mas não sem antes deixar registrado o que vem por ai (sem pretensão de fazer uma lista exaustiva nem definir alguma ordem ou periodicidade):

Até breve (espero).

  1. Existem também implementações que compilam para a CLR e para JavaScript

  2. Nem sempre o primeiro elemento é uma função. Existem também macros e formas especiais, mas a sintaxe não muda. Podemos generalizar e dizer que tudo são funções. 

  3. Tradicionalmente Lisps usam apenas listas para representar código. Clojure usa outras estruturas, como vetores e mapas

  4. Uma forma é qualquer coisa que possa avaliada. Existe uma sútil diferença entre formas e expressões, mas para todos os efeitos podemos usas as duas palavras indistintamente. 

  5. Quaisquer valores diferentes de nil e false serão considerados verdadeiros numa avaliação lógica. É o comportamento já conhecido por programadores Ruby. Em C 0 é considerado falso, em Clojure não. Assim como listas vazias '(), que também indicam falsidade em outros Lisps e indicam verdade em Clojure. 

  6. Mais sobre vetores em outra oportunidade. Por enquanto basta saber que são como listas, mas tem colchetes [], no lugar dos parênteses ()

  7. #' é um exemplo de uma macro de leitura. Simplesmente uma forma mais sucinta de executar alguma função. Nesse caso #'user/pi equivale a (var user/pi). Existem várias outras macros de leitura, que devem ser exploradas em posts mais apropriados. 

  8. É muito comum em JavaScript associarmos funções sem nome a variáveis: var greet = function (name) { return "Hello, " + name; }