Anotações do livro The Joy of Clojure (2nd Edition).
Clojure é uma linguagem simples (não simplória), expressiva e prática (usada fora da academia). Suas maiores qualidades são:
- Simplicidade: síntaxe simples e ausência de construções complexas;
- Consistência: funcionalidades semelhantes possuem síntaxes semelhantes;
- Flexibilidade: possibilidade de extender tipos de terceiros (via protocols), de criar DSLs (domain specific language) facilmente e de alterar a própria estrutura do código, mesmo em tempo de execução;
- Ausência de precedência de operadores devido ao uso de notação pré-fixa (LISP like).
One example of incidental complexity is the tendency of modern object-oriented languages to require that every piece of runnable code be packaged in layers of class definitions, inheritance, and type declarations.
Clojure usa muitos conceitos de linguagens funcionais. Uma ideia central de imutabilidade é a questão de tempo:
Over time, the properties associated with an entity—both static and changing, singular or composite will form a concrescence (Whitehead 1929), or, in other words, its identity. It follows that at any given time, a snapshot can be taken of an entity’s properties, defining its state. This notion of state is an immutable one because it’s not defined as a mutation in the entity itself, but only as a manifestation of its properties at a given moment in time.
É importante notar que em linguagens orientada a objetos, não há uma clara distinção entre estado e identidade.
Vantangens do paradigma funcional são:
- Código previsível: uma expressão (pura) pode ser substituída por um valor devido à ausência de efeitos colaterais;
- Multi-threading: a maior complexidade da computação paralela acontece por causa da concorrência na mutação de estados, não na leitura;
Os passos do read-eval-print loop do Clojure são:
- Read: transformando síntaxe (texto) em estruturas de dados;
- Expand: transformando estrutura de dados, através de macros, em mais estrutura de dados;
- Compile: transformando estrutura de dados em bytecode;
- Eval: avaliando o código gerado.
- Print: exibindo o que foi processado.
(type 50)
;=> java.lang.Long
(type 4454484841315454354543543431335444)
;=> clojure.lang.BitInt
(type 2N)
;=> clojure.lang.BitInt
0xFF ; Hex to 255
031 ; Octal to 25
2r101 ; Binary to 5
(type 12.5)
;=> java.lang.Double
(type (/ 4 2))
;=> java.lang.Long
(type (/ 1 3))
;=> clojure.lang.Ratio
(type (/ 1.0 3))
;=> java.lang.Double
Strings, números e keywords são avaliados para eles mesmos. Símbolos são avaliados para valores que estão a eles associados.
(def my-symbol "hello")
my-symbol
;=> "Hello"
(identical? 'nome 'nome)
;=> false
Avaliam para eles mesmos, são únicos e são muito usado em dicionários.
:key
;=> :key
(def my-dict {:nome "Leandro"})
(:nome my-dict) ; works as a function
;=> Leandro
:nome
;=> :nome
::nome ; qualified to current namespace
;=> user/nome
:namespace-that-doesnt-exists/:nome
;=> :namespace-that-doesnt-exists/:nome
(identical? :nome :nome)
;=> true
Segue abaixo uma interessante comparação entre Keyworkds e Symbols e porque símbolos iguais não são idênticos.
Spoiler: símbolos iguais podem carregar diferentes metadados!
(= 'a 'a)
;=> true
(identical? 'a 'a)
;=> false
(= :a :a)
;=> true
(identical? :a :a)
;=> true
(def firstSymbolWithMeta (with-meta 'abc {:a 1}))
(def secondSymbolWithMeta (with-meta 'abc {:b 2}))
(= firstSymbolWithMeta secondSymbolWithMeta)
;=> true
(identical? firstSymbolWithMeta secondSymbolWithMeta)
;=> false
(meta firstSymbolWithMeta)
;=> {:a 1}
(meta secondSymbolWithMeta)
;=> {:b 2}
(identical? (with-meta 'a {:a 1}) (with-meta 'a {:a 1}))
;=> false
Strings são java.lang.String
"olá"
;=> "olá"
"olá
mundo"
;=> "olá\nmundo"
(type "hello")
;=> java.lang.String
Characters são java.lang.Character
\a
;=> \a
(type \b)
;=> java.lang.Character
Coleções são heterogênicas, ou seja, podem armazenar diferentes tipos de dados.
O primeiro elemento de uma lista é avaliado como uma função, uma macro ou um operador especial.
(str "olá" ", " "mundo")
;=> "olá, mundo"
(type '(+ 1 2))
;=> clojure.lang.PersistentList
Caso o primeiro elemento seja uma função, os argumentos são avaliados sequencialmente antes da avaliação da função; caso seja um operador especial ou uma macro, os argumentos não são avaliados, necessarimente, em ordem.
Os elementos do vector são todos avaliados em ordem.
[1 (+ 1 1) (+ 1 1 1)]
;=> [1 2 3]
(type [1 2 3])
;=> clojure.lang.PersistentVector
Mapas guardam chaves únicas e um item por chave.
(type {:nome "Leandro" :sobrenome "Oliveira"})
;=> clojure.lang.PersistentArrayMap
{["um" "one"] 1, ["dois" "two"] 2} ; A vírgula é avaliada para whitespace
{:nome "leandro" :nome "oliveira"} ; Throws IllegalArgumentException Duplicate key: :nome
Sets guardam elementos sem repetição.
(type #{1 2 3})
;=> clojure.lang.PersistentHashSet
#{1 2 3}
;=> {1 3 2}
#{[1] [1] [2]} ; Throws IllegalArgumentException Duplicate key: [1]
(def sum1 (fn [a b] (+ a b)))
(defn sum2
"This is a doc!"
[a b]
(+ a b))
(defn sum3
([a] a) ; arity = 1
([a b] (+ a b)) ; arity = 2
([a b c] (+ a b c)) ; arity = 3
)
(def sum4 #(+ %1 %2)) ; % é o mesmo que %1, mas é preferível a versão numerada
A forma do faz com que todo bloco seja avaliado como uma única expressão. O bloco abaixo possui 5 passos e o bloco é avaliado para o valor da última expressão.
(do
(def x 5)
(def y 6)
(def s (+ x y))
(println s)
s)
;=> 11
Criação de locals, análago às variáveis locais.
(let
[x 5]
[y 6]
[s (+ x y)]
(println s)
s)
;=> 11
Funções recursivas podem ser otimizadas com tail-call optimization através do uso da forma recur.
(defn recur-function [current a]
"Usando recur como função recursiva"
(if (pos? a)
(recur (+ current a) (dec a)) ; recursividade na função definida
current))
(defn recur-loop-function [value]
"Usando recur como reentrada do loop"
(loop [sum 0 c value] ; sum = 0 e c = value é definido avaliando somente na primeira interação
(if (pos? c)
(recur (+ sum c) (dec c)) ; recursividade do bloco loop
sum)))
Quoting faz com que expressões não sejam avaliadas.
(quote (+ 1 2))
;=> (+ 1 2)
'(+ 1 2)
;=> (+ 1 2)
() ; (), pois a lista vazia não precisa de quoting em Clojure
O uso de quoting é muito comum em linguagens LISP, mas não tanto em Clojure devido a presença de outras estruturas de dados (vectors, maps e sets).
Funciona como quote, porém qualifica os símbolos.
'(+ a b)
;=> (+ a b)
`(+ a b)
;=> (clojure.core/+ user/a user/b)
O uso de ~ antes do uma expressão, dentro de uma syntax-quote, faz com que ela seja avaliada.
; syntax-quote
`(+ 5 (* 3 4))
;=> (clojure.core/+ 5 (clojure.core/* 3 4))
; syntax-quote
`(+ 5 ~(* 3 4))
;=> (clojure.core/+ 5 12)
; quote, NOT syntax-quote
'(+ 5 ~(* 3 4))
;=> (+ 5 (clojure.core/unquote (* 3 4))
(let [x '(2 3)] `(1 ~x))
;=> (1 (2 3))
(let [x '(2 3)] `(1 ~@x))
;=> 1 2 3
(Math/sqrt 35)
;=> 5.916079783099616
(new java.util.HashMap {"nome" "Leandro" "idade" 26})
;=> {"idade" 26, "nome" "Leandro"}
(java.util.HashMap. {"nome" "Leandro" "idade" 26})
;=> {"idade" 26, "nome" "Leandro"}
(.equalsIgnoreCase "a" "A")
;=> true
(let [origin (java.awt.Point. 0 0)]
(set! (.-x origin) 15)
(str origin))
;=> "java.awt.Point[x=15,y=0]"
(.endsWith (.toString (java.util.Date.)) "ABC")
;=> false
(.. (java.util.Date.) toString (endsWith "ABC"))
;=> false
Normalmente, as macros -> e ->> são mais usadas, mesmo em situação de não interoperabilidade com Java.
Usada para invocar métodos seguidos em um mesmo objeto, situação comum em configuração de objetos (padrão builder, por exemplo)
(doto (java.util.HashMap.)
(.put "HOME" "/home/me")
(.put "SRC" "src")
(.put "BIN" "classes"))
;=> {"SRC" "src", "BIN" "classes", "HOME" "/home/me"}
(throw (Exception. "Deu ruim"))
(try
(/ 10 0)
(catch ArithmeticException e "No dividing by zero!")
(catch Exception e (str "You are so bad " (.getMessage e)))
(finally (println "returning... ")))
; returning...
;=> "No dividing by zero!"
(ns hello.world)
;=> Creating namespace hello.world and switching to it
(str "The current namespace is " *ns*)
;=> "The current namespace is hello.world"
Carregado namespace do disco:
(ns joy.req
(:require clojure.set))
(clojure.set/intersection #{1 2 3} #{2 3 4})
;=> #{3 2}
Carregando e dando um apelido ao namespace:
(ns joy.req-alias
(:require [clojure.set :as s]))
(s/intersection #{1 2 3} #{2 3 4})
;=> #{3 2}
Dando um apelido a uma função:
(ns joy.yet-another
(:refer clojure.set :rename {union onion}))
(onion #{1 2 3} #{2 3 4})
;=> #{1 4 3 2}
Como adicionar somente o símbolo clojure.string/capitalize ao namespace atual?
(ns joy.use-ex
(:require [clojure.string :refer (capitalize)]))
(map capitalize ["kilgore" "trout"])
;=> ("Kilgore" "Trout")
(ns joy.java
(:import
[java.util HashMap]
[java.util.concurrent.atomic AtomicLong]))
(HashMap. {"happy?" true})
;=> {"happy?" true}
Apenas o valores false e nil avaliam para false.
(def evil-false (Boolean. "false")) ; NUNCA faça isso
(if evil-false :true :false)
;=> :true
(def good-false (Boolean/valueOf "false")); faça isso
(if good-false :true :false)
;=> :false
A função seq cria uma "view" de uma coleção.
(seq [1 2 3])
;=> (1 2 3)
(seq [])
;=> nil
Como a coleção [] retorna nil na função seq, esta se torna uma aliada para avaliar se a lista está vazia, caso base comum em recursão.
(let [[fname mname lname] ["Leandro" "Gonçalves" "Oliveira"]]
(str lname ", " fname " " mname))
;=> "Oliveira, Leandro Gonçalves"
(let [[a b & c] (range 10)] c)
;=> (2 3 4 5 6 7 8 9)
(let [[a b & c :as all] (range 10)] all)
;=> (0 1 2 3 4 5 6 7 8 9)
(def my-map {:a 1 :b 2 :c 3})
(let [{a :a b :b c :c}
my-map] (+ a b c))
;=> 6
(let [{:keys [a b c]}
my-map] (+ a b c))
;=> 6
(let [{:keys [a b c] :as whole-map} my-map]
whole-map)
;=> {:a 1, :b 2, :c 3}
(let [{:keys [a b c d] :or {d 4}} my-map]
(+ a b c d))
;=> 10 (repare que o d do or não é uma keyword)
(let [{first-thing 0, last-thing 3} [1 2 3 4]]
[first-thing last-thing])
;=> [1 4]
(defn print-last-name
[{:keys [lastName] :or {lastName "No last name"}}]
(println lastName))
(print-last-name {:lastName "Oliveira"})
; Oliveira
;=> nil
(print-last-name {:firstName "Leandro"})
; No last name
;=> nil
(doc conj)
; -------------------------
; clojure.core/conj
; ([coll x] [coll x & xs])
; conj[oin]. Returns a new collection with the xs
; 'added'. (conj nil item) returns (item). The 'addition' may
; happen at different 'places' depending on the concrete type.
; nil
(find-doc "xor")
; -------------------------
; clojure.core/bit-xor
; ([x y] [x y & more])
; Bitwise exclusive or
; nil
(type 1.4)
;=> java.lang.Double
(type 1.434126544356243465432653442654423654462344624324) ; It'll be truncated
;=> java.lang.Double
(type 1.4M)
;=> java.math.BigDecimal
Em Clojure, qualquer operação que envolva um double, mesmo que haja BigDecimal envolvido, será avaliado como double.
Problemas com doubles:
(def a 1.0e50)
(def b -1.0e50)
(def c 17.0e00)
(+ (+ a b) c)
;=> 17.0
(+ a (+ b c))
;=> 0.0
A mesma computação feita com racionais:
(def a (rationalize 1.0e50))
(def b (rationalize -1.0e50))
(def c (rationalize 17.0e00))
(+ (+ a b) c)
;=> 17N
(+ a (+ b c))
;=> 17N
E como saber se um número é racional:
(rational? 1.0e50)
;=> false
(rational? a)
;=> true
Mas lembre-se: o uso de racionais é menos performático que o uso de doubles. Analise os casos que desempenho é mais importante do precisão.
(def my-re #"(?i)[a-z]") ; means (?<options>), where i = ignoreCase
(type my-re)
;=> java.util.regex.Pattern
(re-seq my-re "aBcDe")
;=> ("a" "B" "c" "D" "e")
Há outras funções como re-matcher, re-groups e re-find, mas estão são perigosas, pois, internamente, mutam o objeto Regex.
Clojure é uma implementação Lisp-1. Em simples termos, significa que ela usa as mesmas regras de resolução de nomes tanto dentro quanto fora de funções. Em Lisp-2, a resolução de símbolos depende do contexto no qual o símbolo se encontra (function-call ou function-argument).
Clojure divide suas coleções em 3 categorias: sequentials, maps e sets.
As coleções em Clojure são persistentes, mas não no sentido compreendido atualmente de gravar em algum banco dados, mas no sentido que são imutáveis.
- Sequentials: coleção de dados que guarda valores sem reordená-los;
- Sequences: é uma coleção sequencial que representa valores que podem, ou não, ter sido criados (lazy);
- Seqs: uma API que define as funções first e rest para navegar em coleções.
Experimentando as funções da Seq API:
(first nil)
;=> nil
(first [])
;=> nil
(first [1])
;=> 1
(rest nil)
;=> ()
(rest [])
;=> ()
(rest [1 2])
;=> (2)
É importante ressaltar que funções que retornam sequences, como map e filter, trabalham de maneira análoga a rest.
Coleções de categorias diferentes (sequentials, maps e sets) nunca são iguais, mesmo que tenham os mesmos valores. Coleções da mesma categoria, mas de tipos diferentes, são iguais se possuem que avaliam como iguais. (e na mesma sequência, para sequences).
(= [1 2 3] `(1 2 3))
;=> true
(= [1 2 3] [1 3 2])
;=> false
(= [1 2 3] #{1 2 3})
;=> false
(= ['(1 2)] [[1 2]])
;=> true
Many Lisps build their data types (McCarthy 1962) on the cons-cell abstraction, an elegant two-element structure built by nodes that hold a value and a reference to the next node.
When learning about Clojure’s persistent data structures, you’re likely to hear the term O(log 32 n) for those based on the persistent hash trie and O(log 2 n) for the sorted structures. Accessing an element in a Clojure persistent structure by index is O(log n), or logarithmic.
Vetores são parecidos com arrays, porém são imutáveis. Eles são eficientes (processamento e memória) em pequenos ou grandes tamanhos.
[1 2 3] ; literal
(vec (range 5)) ; argumento deve ser uma seq
;=> [0 1 2 3 4]
(into [-2 -1] (range 5)) ; o primeiro argumento deve ser um vector para o retorno ser um vector
;=> [-2 -1 0 1 2 3 4]
(into (vector-of :int) [Math/PI 13 157.34]) ; Criando vetor de primitivos: :int , :long , :float , :double , :byte , :short , :boolean ou :char
;=> [3 13 157]
Vetores são muito eficientes em três operações, se comparado com listas:
- Adicionar ou remover elemento do final direito;
- Acessando um elemento pelo seu índice (Big-O de log de base 32);
- Andar pelos elementos na ordem inversa.
(def a [0 1 2 3 4])
(def b (assoc a 2 "2")) ; Alterar o elemento do índice 2 por "2"
a
;=> [0 1 2 3 4]
b
;=> [0 1 "2" 3 4]
Vetores podem ser usados como stacks através das funções peek, conj e pop. Ambas trabalham com o elemento mais a direita do vetor.
(def s [1 2 3])
(+ (peek (pop s)) (peek s))
;=> 5
s
;=> [1 2 3]
Operação de subvetor é eficiente (sem cópia adicional) com o uso de subvec.
(def v [0 1 2 3 4 5 6])
(subvec v 2 5) ; eficiente por ser imutável! :)
;=> [2 3 4]
Vetores não são eficientes em:
- Adicionar um antes do último elemento;
- Remover um item que não seja o último elemento;
- Serem usados como filas (por consequência);
Cuidado: a função contains? avalia se um índice existe no vetor ao invés de avaliar se é o valor que existe.
Listas são usadas quase que exclusivamente para representar código, tanto que caso sua finalidade não for essa, vetores oferecem mais vantagens. Comparada a vetores, adicionar ou remover elementos é mais eficiente na ponta esquerda.
For each concrete type, conj adds elements in the most efficient way, and for lists this means at the left side. Additionally, a list built using conj is homogeneous — all the objects on its next chain are guaranteed to be lists, whereas sequences built with cons only promise that the result will be some kind of seq. So you can use cons to add to the front of a lazy seq, a range, or any other type of seq, but the only way to get a bigger list is to use conj.
Assim como vetores, listas podem ser usadas como stacks (conj, peek e pop). Diferente de vetores, buscar um elemento por seu índice é muito ineficientem, Big-O(n).
Queues são criada a partir do valor clojure.lang.PersistentQueue/EMPTY com as funções peek, conj e *pop.
(def q (conj clojure.lang.PersistentQueue/EMPTY :a :b :c))
(peek q)
;=> :a
(peek (pop (pop (pop (conj q :d)))
;=> :d
Infelizmente, a forma como queues são "printadas" no REPL é ruim. Para observar filas de um jeito melhor, use o código abaixo:
(defmethod print-method clojure.lang.PersistentQueue [q, w]
(print-method '<- w)
(print-method (seq q) w)
(print-method '-< w))
Sets funcionam como Hash Sets (sem ordem). Dados dois elementos que avaliam como igual, um Set vai contar apenas um desses elementos, independente do tipo concreto.
(into #{[]} [()])
;=> #{[]}
Sets também podem ser usados como funções.
(#{1 2 3} 3)
;=> 3
(#{1 2 3} 4)
;=> nil
(sorted-set 5 4 2 3 1)
;=> #{1 2 3 4 5}
(sorted-set 5 4 2 "3" 1) ; Throws ClassCastException java.lang.Long cannot be cast to java.lang.String java.lang.String.compareTo
Sets ordenados podem ser criados desde que seus elementos possam sem comparados. A mesma exceção lançada acimada pode ser lançada via conj.
Além disso, sets e sorted-sets avaliam de forma diferente:
(sorted-set 1 2 3 3.0)
;=> #{1 2 3}
#{1 2 3 3.0}
;=> #{3.0 1 3 2}
E também é possível criar sorted-set com comparação customizada:
(sorted-set-by
#(let [[a b] %1 [c d] %2] (if (= a c) (< b d) (> a c)))
[1 2] [3 4] [3 5])
;=> #{[3 4] [3 5] [1 2]}
Mapas tem um papel importante na maioria da linguagens, e mais importante ainda em clojure devido a ausência de classes.
(seq {:nome "Leandro"})
;=> ([:nome "Leandro"])
(into {} [[:nome "Leandro"]])
;=> {:nome "Leandro"}
(zipmap [:nome :sexo] ["Leandro" :masculino])
;=> {:nome "Leandro", :sexo :masculino
Assim como sets, Clojure suporta Sorted Maps através da função sorted-map. Caso as chaves não sejam comparáveis, uma exceção é lançada. Também é possível criar um Sorted Map com comparação customizada através da função sorted-map-by.
Another way that sorted maps and hash maps differ is in their handling of numeric keys. A number of a given magnitude can be represented by many different types; for example, 42 can be a long, an int, a float, and so on. Hash maps treat each of these different objects as different, whereas a sorted map treats them as the same.
Além de Sorted Map, Clojure suporta Array Map, que mantém a ordem das inserções.
(array-map :b 2 :a 1 :c 3)
;=> {:b 2, :a 1, :c 3}
Programação funcional em Clojure é sustentada por dois pilares: imutabilidade e avaliação preguiçosa (lazyness).
Although it’s no panacea, fostering immutability at the language level solves many difficult problems right out of the box while simplifying many others.
Entre as vantangens de programação funcional, podemos destacar:
- Simplicidade na igualdade de objetos: em linguagens onde não há restrição para mutação, dois objetos que são iguais agora podem não ser mais iguais momentos depois.
- Compartilhamento de memória: em linguagens não funcionais, é necessário fazer cópias defensivas para compartilhar objetos.
- Programação paralela: objetos imutáveis podem ser compartilhados entre threads sem medo de erros decorrentes de mutação.
- Previsibilidade: expressões puras podem ser substituídas pelo seus resultados, pois não há efeitos colaterais;
Clojure possui referêncial mutável, mas não oferece objetos mutáveis.
Novos objetos podem ser criados aproveitando objetos imutáveis. Um exemplo simples é a lista encadeada: duas listas diferentes podem ser criadas aproveitando uma lista em comum, sem dobrar o consumo de memória.
Um outro exemplo é a Árvore Binária de Busca, conforme código abaixo.
(defn xconj [root value]
(cond
(nil? root) {:value value :left nil :right nil}
(< value (:value root)) {:value (:value root) :left (xconj (:left root) value) :right (:right root)}
:else {:value (:value root) :left (:left root) :right (xconj (:right root) value)}))
(def a (xconj nil 5))
(def b (xconj a 4))
(def c (xconj b 2))
c
;=> {:value 5, :left {:value 4, :left {:value 2, :left nil, :right nil}, :right nil}, :right nil}
É evidente que partes da árvore são aproveitadas nas formas (:left root)
e (:right root)
. E, apesar de esse exemplo demostrar o poder de estruturas de dados imutáveis, as estruturas de Clojure são muito mais robustas, especialmente por serem combinadas com lazy sequences.
Regras para trabalhar com lazy-seq:
- Use the lazy-seq macro at the outermost level of your lazy sequence–producing expression(s).
- If you happen to be consuming another sequence during your operations, then use rest instead of next .
- Prefer higher-order functions when processing sequences.
- Don’t hold on to your head.
(defn my-lazy-range [i limit]
(lazy-seq
(when (i < limit)
(cons i (my-lazy-range (inc i) limit)))))
Há, também, as formas delay e force:
(defn calc [a b]
(if-let [ra (force a)]
ra
(+ (force b) (force b))))
(calc
:easy-calculation
(delay (do
(println "Starting to calculate")
(Thread/sleep 5000)
10
)))
;=> :easy-calculation
(calc
false
(delay (do
(println "Starting to calculate")
(Thread/sleep 5000)
10
)))
; Starting to calculate
;=> 20
Repare que a computação com Thread/sleep 1000
é executada apenas uma vez.
Uso de comp (compose):
(defn nth
[n]
(apply comp (cons first (take (dec n) (repeat rest)))))
(let [third (nth 3)] (third [1 2 3]))
;=> 3
Uso de partial:
(def add5 (partial + 5))
(add5 10)
;=> 15
Uso de complement
(def my-odd? (complement even?))
(my-odd? 2)
;=> false
(my-odd? 3)
;=> true
Entre as higher-order functions disponíveis em Clojure estão: map, reduce, filter, some, repeatedly, sort, sort-by, keep, take-while e drop-while.
Funções puras devem obedecer duas propriedades:
- Sempre retornará o mesmo resultados dados os mesmo argumentos
- Nenhum efeito colateral observável é causado
(defn slope
[& {:keys [p1 p2] :or {p1 [0 0] p2 [1 1]}}]
(float (/ (- (p2 1) (p1 1))
(- (p2 0) (p1 0)))))
(slope :p2 [2 1])
;=> 0.5
Anteriormente, foi visto de otimização de calda pode ser feita com recur
, porém como fica o caso de funções mutualmente recursivas?
Para casos assim existe a função trampoline
. Ela recebe uma função f e N argumentos. A função f é chamada com os N argumentos; caso o resultado seja uma função, esta função é chamada sem argumentos, caso contrário, o valor é devolvido.
(defn collatz [n]
(letfn [(A [n] #(cond
(= n 1) :done
(even? n) (A (/ n 2))
:else (B (+ (* 3 n) 1))))
(B [n] #(cond
(= n 1) :done
(odd? n) (B (+ (* 3 n) 1))
:else (A (/ n 2))))]
(trampoline (if (even? n) A B) n)))
(collatz 45445899746465487684345468411134846443161413176464354641134876787N)
; (a lot of prints)
;=> :done
Using
letfn
this way allows you to create local functions that reference each other, whereaslet
wouldn’t, because it executes its bindings serially.
Where macros differ from techniques familiar to proponents of Java’s object-oriented style—including hierarchies, frameworks, inversion of control, and the like—is that they’re treated no differently by the language itself. Clojure macros work to mold the language into the problem space rather than force you to mold the problem space into the constructs of the language. There’s a specific term for this, domain-specific language, but in Lisp the distinction between DSL and API is thin to the point of transparency.
Regardless of your application domain and its implementation, programming language boilerplate code inevitably occurs and is a fertile place to hide subtle errors. But identifying these repetitive tasks and writing macros to simplify and reduce or eliminate the tedious copy-paste-tweak cycle can work to reduce the incidental com- plexities inherent in a project. Where macros differ from techniques familiar to pro- ponents of Java’s object-oriented style—including hierarchies, frameworks, inversion of control, and the like—is that they’re treated no differently by the language itself. Clojure macros work to mold the language into the problem space rather than force you to mold the problem space into the constructs of the language.
Esse assunto deve ser estudo mais a fundo.
(ns my.namespace) ; Note que NÃO há quoting
Criar um namespace, importa todas as classes de java.lang, todas as definições de clojure.core e entra nele.
(in-ns 'my.namespace) ; Note que há quoting
Funciona parecido com o ns
, mas não importa as definições de clojure.core.
create-ns
é uma função que cria um namespace e retorna um objeto que representa o namespace recém-criado.
(def b (create-ns 'my-ns)) ; Cria um namespace, mas não vai para ele.
b
;=> #object[clojure.lang.Namespace 0x1814f0f6 "my-ns"]
((ns-map b) 'String) ; Resolvindo um símbolo
;=> java.lang.String
((ns-map b) 'reduce)
;=> nil
(intern b 'reduce 'clojure.core/reduce) ; Injetando um símbolo
;=> #'my-ns/reduce
(intern b '+ 'clojure.core/+)
;=> #'my-ns/+
(my-ns/reduce my-ns/+ [1 2 3])
;=> 6
(remove-ns 'my-ns)
;=> #object[clojure.lang.Namespace 0x6e6f4ced "my-ns"]
Records permitem busca de elementos em O(1) para chaves definidas. Digo por chaves definidas porque é possível extender um record:
(defrecord Pessoa [nome sobrenome])
(def leandro (Pessoa. "Leandro" "Oliveira"))
(assoc leandro :linguagem-favorita "clojure")
;=> #user.Pessoa{:nome Leandro, :sobrenome Oliveira, :linguagem-favorita clojure}
Loading a namespace via :require or :use isn’t enough to import defrecord and deftype classes.
(defrecord BTree [value left right])
(defn bt-conj [node value-to-be-inserted]
(if-let [{:keys [value left right]} node]
(if (< value-to-be-inserted value)
(BTree. value (bt-conj left value-to-be-inserted) right)
(BTree. value left (bt-conj right value-to-be-inserted)))
(BTree. value-to-be-inserted nil nil)))
(bt-conj (bt-conj nil 83) 173)
;=> #user.BTree{:value 83, :left nil, :right #user.BTree{:value 173, :left nil, :right nil}}
(bt-conj (bt-conj nil 339) 173)
;=> #user.BTree{:value 339, :left #user.BTree{:value 173, :left nil, :right nil}, :right nil}
O jeito Clojure de trabalhar com polimorfismo. Os contratos de defprotocol
devem ter pelo menos um argumento, que é usado para o dispatch da implementação correta. Também é possível extender nil
.
(ns learning.clojure)
(defprotocol OgQueue
(og-push [q e])
(og-pop [q])
(og-peek [q]))
;; (extend-type clojure.lang.IPersistentVector OgQueue
;; (og-push [q e]
;; (conj q e))
;; (og-pop [q]
;; (pop q))
;; (og-peek [q]
;; (peek q)))
(extend clojure.lang.IPersistentVector OgQueue
{:og-push conj
:og-pop pop
:og-peek peek})
(og-push (og-push [] 4) 5)
;=> [4 5]
(og-peek [4 5])
;=> 5
(og-pop [4 5])
;=> [4]
É possível gerar código mais que executará com melhor performance ao definir um record junto com suas assinaturas de procotolos:
(defprotocol OgQueue
(og-push [q e])
(og-pop [q])
(og-peek [q]))
(defrecord BTree [value left right]
OgQueue
(og-push [tree value-to-be-inserted]
(if tree
(if (< value-to-be-inserted value) ; Repare que value é usado sem destructuring.
(BTree. value (og-push left value-to-be-inserted) right)
(BTree. value left (og-push right value-to-be-inserted)))
(BTree. value-to-be-inserted nil nil)))
(og-pop [tree]
(if left
(BTree. val (og-pop left) right)
right))
(og-peek [tree]
(if left
(og-peek left)
value)))
(extend-type nil
OgQueue
(og-push [_ value]
(BTree. value nil nil)))
(og-push (og-push nil 5) 10)
;=> #user.BTree{:value 5, :left nil, :right #user.BTree{:value 10, :left nil, :right nil}
Ao definir um record é possível fazer override do tipo Object do Java. Isso é útil para definir .toString().