; Comentários começam por ponto e vírgula ; Código Clojure é escrito em formas - 'forms', em inglês. Tais estruturas são ; simplesmente listas de valores encapsuladas dentro de parênteses, separados por ; espaços em branco. ; Ao interpretar um código em Clojure, o interpretador ou leitor - do inglês 'reader' - assume ; que o primeiro valor dentro de uma forma é uma função ou macro, de modo que os demais valores ; são seus argumentos. Isso se deve ao fato de que Clojure, por ser uma derivação de Lisp, ; usa notação prefixa (ou polonesa). ; Num arquivo, a primeira chamada deve ser sempre para a função ns, ; que é responsável por definir em qual namespace o código em questão ; deve ser alocado (ns learnclojure) ; Alguns exemplos básicos: ; Aqui, str é uma função e "Olá" " " e "Mundo" são seus argumentos. O que ela faz é criar ; uma string concatenando seus argumentos. (str "Olá" " " "Mundo") ; => "Olá Mundo" ; Note que espaços em branco separam os argumentos de uma função. Opcionalmente vírgulas ; podem ser usadas, se você quiser. (str, "Olá", " ", "Mundo") ; => "Olá Mundo" ; As operações matemáticas básicas usam os operadores de sempre (+ 1 1) ; => 2 (- 2 1) ; => 1 (* 1 2) ; => 2 (/ 2 1) ; => 2 ; Esses operadores aceitam um número arbitrário de argumentos (+ 2 2 2) ; = 2 + 2 + 2 => 6 (- 5 1 1) ; = 5 - 1 - 1 => 3 (* 3 3 3 3) ; = 3 * 3 * 3 * 3 => 81 ; Para verificar se dois valores são iguais, o operador = pode ser usado (= 1 1) ; => true (= 2 1) ; => false ; Para saber se dois valores são diferentes (not= 1 2) ; => true (not (= 1 2)) ; => true ; Conforme vimos acima, é possível aninhar duas formas (+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2 (* (- 3 2) (+ 1 2)) ; = (3 - 2) * (1 + 2) => 3 ; Se a leitura ficar comprometida, as fórmulas também podem ser escritas em múltiplas linhas (* (- 3 2) (+ 1 2)) ; => 3 (* (- 3 2) (+ 1 2)) ; => 3 ; Tipos ;;;;;;;;;;;;; ; Por ter interoperabilidade com Java, Clojure usa os tipos de objetos de Java para booleanos, ; strings e números. Para descobrir qual o tipo de um valor, você pode usar a função `class`: (class 1234) ; Literais Integer são java.lang.Long por padrão (class 1.50) ; Literais Float são java.lang.Double (class "oi") ; Strings sempre usam aspas duplas e são java.lang.String (class false) ; Booleanos são java.lang.Boolean ; Tenha cuidado, ao dividir valores inteiros: (= (/ 1 2) (/ 1.0 2.0)) ; => false (class (/ 1 2)) ; => clojure.lang.Ratio (class (/ 1.0 2.0)) ; => java.lang.Double ; Aqui temos uma diferença em relação a Java, pois valores nulos são representados por `nil` (class nil) ; nil ; Coleções e sequências ;;;;;;;;;;;;;;;;;;; ; Os dois tipos básicos de coleção são listas - "list" em inglês - e vetores - "vectors" ; no original. A principal diferença entre eles se ; dá pela implementação: ; - Vetores são implementados como arrays ; - Listas são listas ligadas (class [1 2 3]) ; => clojure.lang.PersistentVector (class '(1 2 3)) ; => clojure.lang.PersistentList ; Outra forma de declarar listas é usando a função list (list 1 2 3) ; => '(1 2 3) ; Clojure classifica conjuntos de dados de duas maneiras ; "Coleções" são grupos simples de dados ; Tanto listas quanto vetores são coleções: (coll? '(1 2 3)) ; => true (coll? [1 2 3]) ; => true ; "Sequências" (seqs) são descrições abstratas de listas de dados. ; Sequências - ou seqs - são conjuntos de dados com avaliação "lazy" ; Apenas listas são seqs: (seq? '(1 2 3)) ; => true (seq? [1 2 3]) ; => false ; Ter avaliação lazy significa que uma seq somente precisa prover uma informação quando ; ela for requisitada. Isso permite às seqs representar listas infinitas. (range) ; => (0 1 2 3 4 ...) (cycle [1 2]) ; => (1 2 1 2 1 2 ...) (take 4 (range)) ; => (0 1 2 3) ; A função cons é usada para adicionar um item ao início de uma lista ou vetor: (cons 4 [1 2 3]) ; => (4 1 2 3) (cons 4 '(1 2 3)) ; => (4 1 2 3) ; Já conj adiciona um item em uma coleção sempre do jeito mais eficiente. ; Em listas, isso significa inserir no início. Já em vetores, ao final. (conj [1 2 3] 4) ; => [1 2 3 4] (conj '(1 2 3) 4) ; => (4 1 2 3) ; Concatenação de coleções pode ser feita usando concat. Note que ela sempre gera uma ; seq como resultado e está sujeita a problemas de perfomance em coleções grandes, por ; conta da natureza lazy das seqs. (concat '(1 2) [3 4]) ; => (1 2 3 4) (concat [1 2] '(3 4)) ; => (1 2 3 4) ; Outra forma de concatenar coleções é usando into. Ela não está sujeita a problemas ; com a avaliação lazy, mas o resultado final da ordem e do tipo dos argumentos passados (into [1 2] '(3 4)) ; => [1 2 3 4] (into '(1 2) [3 4]) ; => (4 3 1 2) ; Note que em into a ordem dos parâmetros influencia a coleção final. (into [1 2] '(3 4)) ; => (1 2 3 4) (into '(1 2) [3 4]) ; => (4 3 1 2) ; As funções filter e map podem ser usadas para interagir com as coleções. Repare que ; elas sempre retornam seqs, independentemente do tipo do seu argumento. (map inc [1 2 3]) ; => (2 3 4) (filter even? [1 2 3 4]) ; => (2 4) ; Use reduce reduzir coleções a um único valor. Também é possível passar um argumento ; para o valor inicial das operações (reduce + [1 2 3]) ; = (+ (+ (+ 1 2) 3) 4) => 10 (reduce + 10 [1 2 3 4]) ; = (+ (+ (+ (+ 10 1) 2) 3) 4) => 20 (reduce conj [] '(3 2 1)) ; = (conj (conj (conj [] 3) 2) 1) => [3 2 1] ; Reparou na semelhança entre listas e as chamadas de código Clojure? Isso se deve ao ; fato de que todo código clojure é escrito usando listas. É por isso que elas sempre ; são declaradas com o caracter ' na frente. Dessa forma o interpretador não tenta ; avaliá-las. '(+ 2 3) ; cria uma lista com os elementos +, 2 e 3 (+ 2 3) ; o interpretador chama a função + passando como argumentos 2 e 3 ; Note que ' é apenas uma abreviação para a função quote. (quote (1 2 3)) ; => '(1 2 3) ; É possível passar uma lista para que o interpretador a avalie. Note que isso está ; sujeito ao primeiro elemento da lista ser um literal com um nome de uma função válida. (eval '(+ 2 3)) ; => 5 (eval '(1 2 3)) ; dá erro pois o interpretador tenta chamar a função 1, que não existe ; Funções ;;;;;;;;;;;;;;;;;;;;; ; Use fn para criar novas funções. Uma função sempre retorna sua última expressão. (fn [] "Olá Mundo") ; => fn ; Para executar suas funções, é preciso chamá-las, envolvendo-as em parênteses. ((fn [] "Olá Mundo")) ; => "Olá Mundo" ; Como isso não é muito prático, você pode nomear funções atribuindo elas a literais. ; Isso torna muito mais fácil chamá-las: (def ola-mundo (fn [] "Olá Mundo")) ; => fn (ola-mundo) ; => "Olá Mundo" ; Você pode abreviar esse processo usando defn: (defn ola-mundo [] "Olá Mundo") ; Uma função pode receber uma lista de argumentos: (defn ola [nome] (str "Olá " nome)) (ola "Jonas") ; => "Olá Jonas" ; É possível criar funções que recebam multivariadas, isto é, que aceitam números ; diferentes de argumentos: (defn soma ([] 0) ([a] a) ([a b] (+ a b))) (soma) ; => 0 (soma 1) ; => 1 (soma 1 2) ; => 3 ; Funções podem agrupar argumentos extras em uma seq: (defn conta-args [& args] (str "Você passou " (count args) " argumentos: " args)) (conta-args 1 2 3 4) ; => "Você passou 4 argumentos: (1 2 3 4)" ; Você pode misturar argumentos regulares e argumentos em seq: (defn ola-e-conta [nome & args] (str "Olá " nome ", você passou " (count args) " argumentos extras")) (ola-e-conta "Maria" 1 2 3 4) ; => "Olá Maria, você passou 4 argumentos extras" ; Nos exemplos acima usamos def para associar nomes a funções, mas poderíamos usá-lo ; para associar nomes a quaisquer valores: (def xis :x) xis ; => :x ; Inclusive, tais literais podem possuir alguns caracteres não usuais em outras linguagens: (def *num-resposta* 42) (def conexao-ativa? true) (def grito-de-medo! "AAAAAAA") (def ->vector-vazio []) ; É possível, inclusive, criar apelidos a nomes que já existem: (def somar! soma) (somar! 41 1) ; => 42 ; Uma forma rápida de criar funções é por meio de funções anônimas. Elas são ótimas ; para manipulação de coleções e seqs, já que podem ser passadas para map, filter ; e reduce. Nessas funções, % é substituído por cada um dos items na seq ou na coleção: (filter #(not= % nil) ["Joaquim" nil "Maria" nil "Antônio"]) ; => ("Joaquim" "Maria" "Antônio") (map #(* % (+ % 2)) [1 2]) ; => (3 8) ; Mapas ;;;;;;;;;; ; Existem dois tipos de mapas: hash maps e array maps. Ambos compartilham uma mesma ; interface e funções. Hash maps são mais rápidos para retornar dados, mas não mantém ; as chaves ordenadas. (class {:a 1 :b 2 :c 3}) ; => clojure.lang.PersistentArrayMap (class (hash-map :a 1 :b 2 :c 3)) ; => clojure.lang.PersistentHashMap ; Clojure converte automaticamente array maps em hash maps, por meio da maioria das ; funções de manipulação de mapas, caso eles fiquem grandes o suficiente. Não é ; preciso se preocupar com isso. ; Chaves podem ser qualquer valor do qual possa ser obtido um hash, mas normalmente ; usam-se keywords como chave, por possuírem algumas vantagens. (class :a) ; => clojure.lang.Keyword ; Keywords são como strings, porém, duas keywords de mesmo valor são sempre armazenadas ; na mesma posição de memória, o que as torna mais eficientes. (identical? :a :a) ; => true (identical? (String. "a") (String. "a")) ; => false (def mapa-strings {"a" 1 "b" 2 "c" 3}) mapa-strings ; => {"a" 1, "b" 2, "c" 3} (def mapa-keywords {:a 1 :b 2 :c 3}) mapa-keywords ; => {:a 1, :c 3, :b 2} ; Você pode usar um mapa como função para recuperar um valor dele: (mapa-strings "a") ; => 1 (mapa-keywords :a) ; => 1 ; Se a chave buscada for uma keyword, ela também pode ser usada como função para recuperar ; valores. Note que isso não funciona com strings. (:b mapa-keywords) ; => 2 ("b" mapa-strings) ; => java.lang.String cannot be cast to clojure.lang.IFn ; Se você buscar uma chave que não existe, Clojure retorna nil: (mapa-strings "d") ; => nil ; Use assoc para adicionar novas chaves em um mapa. (def mapa-keywords-estendido (assoc mapa-keywords :d 4)) mapa-keywords-estendido ; => {:a 1, :b 2, :c 3, :d 4} ; Mas lembre-se que tipos em Clojure são sempre imutáveis! Isso significa que o mapa ; inicial continua com as mesmas informações e um novo mapa, com mais dados, é criado ; a partir dele mapa-keywords ; => {:a 1, :b 2, :c 3} ; assoc também pode ser usado para atualizar chaves: (def outro-mapa-keywords (assoc mapa-keywords :a 0)) outro-mapa-keywords ; => {:a 0, :b 2, :c 3} ; Use dissoc para remover chaves (dissoc mapa-keywords :a :b) ; => {:c 3} ; Mapas também são coleções - mas não seqs! (coll? mapa-keywords) ; => true (seq? mapa-keywords) ; => false ; É possível usar filter, map e qualquer outra função de coleções em mapas. ; Porém a cada iteração um vetor no formato [chave valor] vai ser passado como ; argumento. Por isso é conveniente usar funções anônimas. (filter #(odd? (second %)) mapa-keywords) ; => ([:a 1] [:c 3]) (map #(inc (second %)) mapa-keywords) ; => (2 3 4) ; Conjuntos ;;;;;; ; Conjuntos são um tipo especial de coleções que não permitem elementos repetidos. ; Eles podem ser criados com #{} ou com a função set. (set [1 2 3 1 2 3 3 2 1 3 2 1]) ; => #{1 2 3} (class #{1 2 3}) ; => clojure.lang.PersistentHashSet ; Note que nem sempre um set vai armazenar seus elementos na ordem esperada. (def meu-conjunto #{1 2 3}) meu-conjunto ; => #{1 3 2} ; Adição funciona normalmente com conj. (conj meu-conjunto 4) ; => #{1 4 3 2} ; Remoção, no entanto, precisa ser feita com disj: (disj meu-conjunto 1) ; => #{3 2} ; Para saber se um elemento está em um conjunto, use-o como função. Nesse aspecto ; conjuntos funcionam de maneira semelhante a mapas. (meu-conjunto 1) ; => 1 (meu-conjunto 4) ; => nil ; Condicionais e blocos ;;;;;;;;;;;;;;;;; ; Você pode usar um bloco let para criar um escopo local, no qual estarão disponíveis ; os nomes que você definir: (let [a 1 b 2] (+ a b)) ; => 3 (let [cores {:yellow "Amarelo" :blue "Azul"} nova-cor :red nome-cor "Vermelho"] (assoc cores nova-cor nome-cor)) ; => {:yellow "Amarelo", :blue "Azul", :red "Vermelho"} ; Formas do tipo if aceitam três argumentos: a condição de teste, o comando a ser ; executado caso a condição seja positiva; e o comando para o caso de ela ser falsa. (if true "a" "b") ; => "a" (if false "a" "b") ; => "b" ; Opcionalmente você pode não passar o último argumento, mas se a condição for falsa ; o if vai retornar nil. (if false "a") ; => nil ; A forma if somente aceita um comando para ser executado em cada caso. Se você ; precisar executar mais comandos, você pode usar a função do: (if true (do (print "Olá ") (print "Mundo"))) ; => escreve "Olá Mundo" na saída ; Se você só deseja tratar o caso de sua condição ser verdadeira, o comando when é ; uma alternativa melhor. Seu comportamento é idêntico a um if sem condição negativa. ; Uma de suas vantagens é permitir a execução de vários comandos sem exigir do: (when true "a") ; => "a" (when true (print "Olá ") (print "Mundo")) ; => também escreve "Olá Mundo" na saída ; Isso ocorre porque when possui um bloco do implícito. O mesmo se aplica a funções e ; comandos let: (defn escreve-e-diz-xis [nome] (print "Diga xis, " nome) (str "Olá " nome)) (escreve-e-diz-xis "João") ;=> "Olá João", além de escrever "Diga xis, João" na saída. (let [nome "Nara"] (print "Diga xis, " nome) (str "Olá " nome)) ;=> "Olá João", além de escrever "Diga xis, João" na saída. ; Módulos ;;;;;;;;;;;;;;; ; Você pode usar a função use para carregar todas as funções de um módulo. (use 'clojure.set) ; Agora nós podemos usar operações de conjuntos definidas nesse módulo: (intersection #{1 2 3} #{2 3 4}) ; => #{2 3} (difference #{1 2 3} #{2 3 4}) ; => #{1} ; Isso porém não é uma boa prática pois dificulta saber de qual módulo cada função ; veio, além de expor o código a conflitos de nomes, caso dois módulos diferentes ; definam funções com o mesmo nome. A melhor forma de referenciar módulos é por meio ; de require: (require 'clojure.string) ; Com isso podemos chamar as funções de clojure.string usando o operador / ; Aqui, o módulo é clojure.string e a função é blank? (clojure.string/blank? "") ; => true ; Porém isso não é muito prático, por isso é possível dar para um nome mais curto para ; o módulo ao carregá-lo: (require '[clojure.string :as str]) (str/replace "alguém quer teste?" #"[aeiou]" str/upper-case) ; => "AlgUém qUEr tEstE?" ; Nesse exemplo usamos também a construção #"", que delimita uma expressão regular. ; É possível carregar outros módulos direto na definição do namespace. Note que nesse ; contexto não é preciso usar ' antes do vetor que define a importação do módulo. (ns test (:require [clojure.string :as str] [clojure.set :as set])) ; Operadores thread ;;;;;;;;;;;;;;;;; ; Uma das funções mais interessantes de clojure são os operadores -> e ->> - respectivamente ; thread-first e thread-last macros. Elas permitem o encadeamento de chamadas de funções, ; sendo perfeitas para melhorar a legibilidade em transformações de dados. ; -> usa o resultado de uma chamada como o primeiro argumento da chamada à função seguinte: (-> " uMa StRIng com! aLG_uNs ##problemas. " (str/replace #"[!#_]" "") (str/replace #"\s+" " ") str/trim ; se a função só aceitar um argumento, não é preciso usar parênteses (str/lower-case)) ; => "uma string com alguns problemas." ; Na thread uma string com vários problemas foi passada como primeiro argumento à função ; str/replace, que criou uma nova string, a partir da original, porém somente com caracteres ; alfabéticos. Essa nova string foi passada como primeiro argumento para a chamada str/replace ; seguinte, que criou uma nova string sem espaços duplos. Essa nova string foi então passada ; como primeiro argumento para str/trim, que removeu espaços de seu início e fim, passando essa ; última string para str/lower-case, que a converteu para caracteres em caixa baixa. ; ->> é equivalente a ->, porém o retorno de cada função é passado como último argumento da ; função seguinte. Isso é particularmente útil para lidar com seqs, já que as funções que ; as manipulam sempre as tomam como último argumento. (->> '(1 2 3 4) (filter even?) ; => '(2 4) (map inc) ; => '(3 5) (reduce *)) ; => 15 ; Java ;;;;;;;;;;;;;;;;; ; A biblioteca padrão de Java é enorme e possui inúmeros algoritmos e estruturas de ; dados já implementados. Por isso é bastante conveniente saber como usá-la dentro ; de Clojure. ; Use import para carregar um módulo Java. (import java.util.Date) ; Você pode importar classes Java dentro de ns também: (ns test (:import java.util.Date java.util.Calendar java.util.ArrayList)) ; Use o nome da clase com um "." no final para criar uma nova instância (def instante (Date.)) (class instante) => ; java.util.Date ; Para chamar um método, use o operador . com o nome do método. Outra forma é ; usar simplesmente . (. instante getTime) ; => retorna um inteiro representando o instante (.getTime instante) ; => exatamente o mesmo que acima ; Para chamar métodos estáticos dentro de classes Java, use / (System/currentTimeMillis) ; => retorna um timestamp ; Note que não é preciso importar o módulo System, pois ele está sempre presente ; Caso queira submeter uma instância de uma classe mutável a uma sequência de operações, ; você pode usar a função doto. Ela é funciona de maneira semelhante à função -> - ou ; thread-first -, exceto pelo fato de que ele opera com valores mutáveis. (doto (java.util.ArrayList.) (.add 11) (.add 3) (.add 7) (java.util.Collections/sort)) ; => # ; STM ;;;;;;;;;;;;;;;;; ; Até aqui usamos def para associar nomes a valores. Isso, no entanto, possui algumas ; limitações, já que, uma vez definido essa associação, não podemos alterar o valor ; para o qual um nome aponta. Isso significa que nomes definidos com def não se ; comportam como as variáveis de outras linguagens. ; Para lidar com estado persistente e mutação de valores, Clojure usa o mecanismo Software ; Transactional Memory. O atom é o mais simples de todos. Passe pra ele um valor inicial e ; e ele criará um objeto que é seguro de atualizar: (def atom-mapa (atom {})) ; Para acessar o valor de um atom, você pode usar a função deref ou o operador @: @atom-mapa ; => {} (deref atom-mapa) ; => {} ; Para mudar o valor de um atom, você deve usar a função swap! ; O que ela faz é chamar a função passada usando o atom como seu primeiro argumento. Com ; isso, ela altera o valor do atom de maneira segura. (swap! atom-mapa assoc :a 1) ; Atribui a atom-mapa o resultado de (assoc {} :a 1) (swap! atom-mapa assoc :b 2) ; Atribui a atom-mapa o resultado de (assoc {:a 1} :b 2) ; Observe que essas chamadas alteraram de fato o valor de atom-mapa. Seu novo valor é: @atom-mapa ; => {:a 1 :b 2} ; Isso é diferente de fazer: (def atom-mapa-2 (atom {})) (def atom-mapa-3 (assoc @atom-mapa-2 :a 1)) ; Nesse exemplo, atom-mapa-2 permanece com o seu valor original e é gerado um novo mapa, ; atom-mapa-3, que contém o valor de atom-mapa-2 atualizado. Note que atom-mapa-3 é um ; simples mapa, e não uma instância de um atom @atom-mapa-2 ; => {} atom-mapa-3 ; => {:a 1} (class atom-mapa-2) ; => clojure.lang.Atom (class atom-mapa-3) ; => clojure.lang.PersistentArrayMap ; A ideia é que o valor do atom só será atualizado se, após ser executada a função passada ; para swap!, o atom ainda estiver com o mesmo valor de antes. Isto é, se durante a execução ; da função alguém alterar o valor do atom, swap! reexecutará a função recebida usando o valor ; atual do átoma como argumento. ; Isso é ótimo em situações nas quais é preciso garantir a consistência de algum valor - tais ; como sistemas bancários e sites de compra. Para mais exemplos e informações sobre outras ; construções STM: ; Exemplos e aplicações: https://www.braveclojure.com/zombie-metaphysics/ ; Refs: http://clojure.org/refs ; Agents: http://clojure.org/agents