Skip to content

Instantly share code, notes, and snippets.

@olegon
Last active May 29, 2019 11:35
Show Gist options
  • Save olegon/875d2a04ead303849d59b10ad9ca96bc to your computer and use it in GitHub Desktop.
Save olegon/875d2a04ead303849d59b10ad9ca96bc to your computer and use it in GitHub Desktop.

The Joy of Clojure

Anotações do livro The Joy of Clojure (2nd Edition).

Clojure Philosophy

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).

Functional

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;

REPL (Read Eval Print Loop)

Os passos do read-eval-print loop do Clojure são:

  1. Read: transformando síntaxe (texto) em estruturas de dados;
  2. Expand: transformando estrutura de dados, através de macros, em mais estrutura de dados;
  3. Compile: transformando estrutura de dados em bytecode;
  4. Eval: avaliando o código gerado.
  5. Print: exibindo o que foi processado.

Clojures Types

Scalar Types

(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

Symbols

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

Keywords

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

Strings são java.lang.String

"olá"
;=> "olá"

"olá
mundo"
;=> "olá\nmundo"

(type "hello")
;=> java.lang.String

Characters

Characters são java.lang.Character

\a
;=> \a

(type \b)
;=> java.lang.Character

Collections

Coleções são heterogênicas, ou seja, podem armazenar diferentes tipos de dados.

Lists -> ()

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.

Vectors -> []

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

Maps -> {}

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 -> #{}

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] 

Funções

(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

Blocos

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

Locals

Criação de locals, análago às variáveis locais.

(let
    [x 5]
    [y 6]
    [s (+ x y)]
    (println s)
    s)
;=> 11

Loops

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

Quoting faz com que expressões não sejam avaliadas.

Quote -> '

(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).

Syntax-quote -> ` (back-quote character)

Funciona como quote, porém qualifica os símbolos.

'(+ a b)
;=> (+ a b)

`(+ a b)
;=> (clojure.core/+ user/a user/b)

Unquote

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

JVM Interop

Static members

(Math/sqrt 35)
;=> 5.916079783099616

Creating instances

(new java.util.HashMap {"nome" "Leandro" "idade" 26})
;=> {"idade" 26, "nome" "Leandro"}

(java.util.HashMap. {"nome" "Leandro" "idade" 26})
;=> {"idade" 26, "nome" "Leandro"}

Acessing instance members

(.equalsIgnoreCase "a" "A")
;=> true

Settings instance fields

(let [origin (java.awt.Point. 0 0)]
    (set! (.-x origin) 15)
    (str origin))
    ;=> "java.awt.Point[x=15,y=0]"

The .. macro (chaining)

(.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.

The doto macro

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"}

Exceptions

Throwing exceptions

(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!"

Namespaces

Creating namespace using ns

(ns hello.world)
;=> Creating namespace hello.world and switching to it

(str "The current namespace is " *ns*)
;=> "The current namespace is hello.world"

Loading namespaces with :require

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}

Loading and creating mappings with :refer

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")

Loading Java classes with :import

(ns joy.java
    (:import
        [java.util HashMap]
        [java.util.concurrent.atomic AtomicLong]))

(HashMap. {"happy?" true})
;=> {"happy?" true}

Detalhes da linguagem

Truthiness

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

Nil pun

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.

Destructuring

Destructuring with a vector

(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)

Destructuring with a map

(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]

Destructuring in function parameters

(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

REPL

(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

More about Scalar Types

Precision

(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.

Rationals

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.

Regular Expressions

(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.

Lisp-1 vs Lisp-2

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).

More about Collections

Clojure divide suas coleções em 3 categorias: sequentials, maps e sets.

What is Persistent?

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.

Seqs, Sequentials and Sequences

  • 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.

Equality Partitions

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

The Sequence Abstraction

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.

Big-O

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.

Vectors

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:

  1. Adicionar ou remover elemento do final direito;
  2. Acessando um elemento pelo seu índice (Big-O de log de base 32);
  3. 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:

  1. Adicionar um antes do último elemento;
  2. Remover um item que não seja o último elemento;
  3. 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.

Lists

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

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))

Persistent Sets

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 Sets

(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]}

Maps

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}

Functional Programming Techniques

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.

Structural sharing: a persistent toye

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.

Laziness

Regras para trabalhar com lazy-seq:

  1. Use the lazy-seq macro at the outermost level of your lazy sequence–producing expression(s).
  2. If you happen to be consuming another sequence during your operations, then use rest instead of next .
  3. Prefer higher-order functions when processing sequences.
  4. 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.

Functional Programming Patterns

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.

Pure functions

Funções puras devem obedecer duas propriedades:

  1. Sempre retornará o mesmo resultados dados os mesmo argumentos
  2. Nenhum efeito colateral observável é causado

Named arguments

(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

Trampoline

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, whereas let wouldn’t, because it executes its bindings serially.

Macros

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.

Namespaces

ns

(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.

ns

(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

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

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}

Protocols

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().

Mutation and concurrency

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment