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:
- primariamente funcional;
- dinâmica;
- fortemente tipada;
- homoicônica;
Além de ter suas características próprias:
- roda na JVM 1;
- foco na imutabilidade;
- primitivas de concorrência/paralelismo poderosas;
- criada por Rich Hickey (isso parece ser uma feature também).
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.
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:
-
É possível ter mais que 2 operandos em cada operação.
(+ 1 2 3 4 5) ;; => 15 ;; o mesmo que 1 + 2 + 3 + 4 + 5 em outras linguagens (< x y z w) ;; em outras linguagens: (x < y) and (y < z) and (z < w)
-
Não é preciso decorar tabelas de precedências, uma vez que todas as expressões são bem delimitadas e sem riscos de ambiguidade.
// JavaScript 4 * 5 + 10 // = 30 10 + 5 * 4 // = 30. // talvez não o que se espere lendo da esquerda pra direita (10 + 5) * 4 // = 60. // precisamos delimitar a soma pra que ela ocorra primeiro
;; Clojure (* (+ 10 5) 4) ; não há como ser ambíguo aqui ... (* 4 (+ 10 5)) ; ... mesmo trocando a ordem dos parâmetros
-
Listas são usadas para escrever o código3 e, ao mesmo tempo, são as estruturas de dados utilizadas para representar o programa em tempo de execução. Este paralelo entre sintaxe e a estrutura interna do programa, que chamamos de homoiconicidade, permite que código seja criado e manipulado como dados. Linguagens homoicônicas facilitam a metaprogramação e reflexão (temas para um outro post).
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
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 lê 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.
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):
- listas, vetores, conjuntos e mapas;
- loops e recursividade;
- organizando código em namespaces;
- fazendo as coisas do jeito funcional;
- trabalhando com imutabilidade;
- paralelismo e concorrência;
- Clojure(Script) conversando com o mundo Java(Script);
- macros: código que manipula código;
- desenvolvimento web com Clojure;
- ferramentas de build:
leiningen
eboot
; - como escrever testes;
- Single Page Applications com ClojureScript (e React?);
- sua sugestão.
Até breve (espero).
-
Existem também implementações que compilam para a CLR e para JavaScript. ↩
-
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. ↩
-
Tradicionalmente Lisps usam apenas listas para representar código. Clojure usa outras estruturas, como vetores e mapas. ↩
-
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. ↩
-
Quaisquer valores diferentes de
nil
efalse
serão considerados verdadeiros numa avaliação lógica. É o comportamento já conhecido por programadores Ruby. Em C0
é considerado falso, em Clojure não. Assim como listas vazias'()
, que também indicam falsidade em outros Lisps e indicam verdade em Clojure. ↩ -
Mais sobre vetores em outra oportunidade. Por enquanto basta saber que são como listas, mas tem colchetes
[]
, no lugar dos parênteses()
. ↩ -
#'
é 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. ↩ -
É muito comum em JavaScript associarmos funções sem nome a variáveis:
var greet = function (name) { return "Hello, " + name; }
↩