Introdução ao Clojure

Introdução ao Clojure

*1. Introdução *

Clojure é uma linguagem de programação funcional que roda inteiramente na Java Virtual Machine, de maneira semelhante ao Scala e Kotlin.* Clojure é considerado um derivado do Lisp *e será familiar para qualquer pessoa que tenha experiência com outras linguagens do Lisp.

Este tutorial fornece uma introdução à linguagem Clojure, apresentando como começar com ela e alguns dos principais conceitos de como ela funciona.

===* 2. Instalando o Clojure *

*O Clojure está disponível como instalador e scripts de conveniência para uso no Linux e macOS* . Infelizmente, nesta fase, o Windows não possui esse instalador.

No entanto, os scripts do Linux podem funcionar em algo como Cygwin ou Windows Bash. Também existe um serviço online que pode ser usado para testar o idioma , e as versões mais antigas possuem uma versão autônoma que pode ser usada.

2.1 Download autônomo

O arquivo JAR independente pode ser baixado em Maven Central. Infelizmente, as versões mais recentes que a 1.8.0 não funcionam mais dessa maneira facilmente, porque o arquivo JAR foi dividido em módulos menores.

Depois que o arquivo JAR é baixado, podemos usá-lo como um REPL interativo simplesmente tratando-o como um JAR executável:

$ java -jar clojure-1.8.0.jar
Clojure 1.8.0
user=>

2.2 Interface da Web para REPL

Uma interface da web para o Clojure REPL está disponível em https://repl.it/languages/clojure para que possamos tentar sem precisar fazer o download de nada. Atualmente, isso suporta apenas o Clojure 1.8.0 e não as versões mais recentes.

2.3 Instalador no MacOS

Se você usa o macOS e o Homebrew instalado, a versão mais recente do Clojure pode ser instalada facilmente:

$ brew install clojure

Isso suportará a versão mais recente do Clojure - 1.10.0 no momento da escrita. Uma vez instalado, podemos carregar o REPL simplesmente usando os comandos clojure ou clj:

$ clj
Clojure 1.10.0
user=>

2.4. Instalador no Linux

Um shell script de instalação automática está disponível para instalar as ferramentas no Linux:

$ curl -O https://download.clojure.org/install/linux-install-1.10.0.411.sh
$ chmod +x linux-install-1.10.0.411.sh
$ sudo ./linux-install-1.10.0.411.sh

Assim como no instalador do macOS, eles estarão disponíveis para as versões mais recentes do Clojure e podem ser executadas usando os comandos clojure ou clj.

*3. Introdução ao Clojure REPL *

Todas as opções acima nos dão acesso ao Clojure REPL. Esse é o equivalente direto do Clojure da https://www..com/java-9-repl [ferramenta JShell] para Java 9 e superior e nos permite inserir o código Clojure e ver o resultado imediatamente diretamente.* Esta é uma maneira fantástica de experimentar e descobrir como certos recursos de linguagem funcionam. *

Depois que o REPL for carregado, teremos um prompt no qual qualquer código Clojure padrão pode ser inserido e executado imediatamente. Isso inclui construções simples do Clojure, bem como interação com outras bibliotecas Java - embora elas precisem estar disponíveis no caminho de classe a ser carregado.

O prompt do REPL é uma indicação do namespace atual em que estamos trabalhando. Para a maioria do nosso trabalho, este é o namespace user e, portanto, o prompt será:

user=>
*Todo o restante deste artigo pressupõe que temos acesso ao Clojure REPL e todos trabalharão diretamente em qualquer ferramenta desse tipo.*

*4. Noções básicas de idioma *

A linguagem Clojure parece muito diferente de muitas outras linguagens baseadas em JVM e, possivelmente, parecerá muito incomum para começar. É considerado um dialeto de Lisp e possui sintaxe e funcionalidade muito semelhantes a outras linguagens Lisp.

*Muito do código que escrevemos no Clojure - como em outros dialetos Lisp - é expresso na forma de Listas* . As listas podem ser avaliadas para produzir resultados - na forma de mais listas ou valores simples.

Por exemplo:

(+ 1 2) ; = 3

Esta é uma lista composta por três elementos. O símbolo "+" indica que estamos realizando esta adição de chamada. Os elementos restantes são então utilizados com esta chamada. Assim, isso avalia como "1 + 2".

*Ao usar uma sintaxe da lista aqui, isso pode ser estendido trivialmente* . Por exemplo, podemos fazer:
(+ 1 2 3 4 5) ; = 15

E isso avalia como "1 + 2 + 3 + 4 + 5".

Observe também o caractere ponto-e-vírgula. Isso é usado no Clojure para indicar um comentário e não é o fim da expressão como veríamos em Java.

4.1. Simples Tipos

*O Clojure é construído sobre a JVM e, como tal, temos acesso aos mesmos tipos padrão que qualquer outro aplicativo Java* . Normalmente, os tipos são inferidos automaticamente e não precisam ser especificados explicitamente.

Por exemplo:

123 ; Long
1.23 ; Double
"Hello" ; String
true ; Boolean

Também podemos especificar alguns tipos mais complicados, usando prefixos ou sufixos especiais:

42N ; clojure.lang.BigInt
3.14159M ; java.math.BigDecimal
1/3 ; clojure.lang.Ratio
#"[A-Za-z]+" ; java.util.regex.Pattern

Observe que o tipo clojure.lang.BigInt é usado em vez de java.math.BigInteger. Isso ocorre porque o tipo Clojure possui algumas otimizações e correções menores.

4.2 Palavras-chave e símbolos

*Clojure nos dá o conceito de palavras-chave e símbolos* . As palavras-chave referem-se apenas a si mesmas e geralmente são usadas para itens como chaves de mapa. Símbolos, por outro lado, são nomes usados ​​para se referir a outras coisas. Por exemplo, definições de variáveis ​​e nomes de funções são símbolos.

Podemos construir palavras-chave usando um nome prefixado com dois pontos:

user=> :kw
:kw
user=> :a
:a

As palavras-chave têm igualdade direta consigo mesmas e não com mais nada:

user=> (= :a :a)
true
user=> (= :a :b)
false
user=> (= :a "a")
false

A maioria das outras coisas no Clojure que não são valores simples são consideradas símbolos. Eles avaliam o que eles se referem , enquanto uma palavra-chave sempre se avalia:

user=> (def a 1)
#'user/a
user=> :a
:a
user=> a
1

4.3 Namespaces

A linguagem Clojure tem o conceito de namespaces para organizar nosso código. Todo código que escrevemos vive em um espaço para nome.

Por padrão, o REPL é executado no namespace user - como visto pelo prompt informando “user ⇒”.

*Podemos criar e alterar namespaces usando a palavra-chave _ns_* :
user=> (ns new.ns)
nil
new.ns=>

Depois de alterar os namespaces, qualquer coisa definida no antigo não estará mais disponível para nós, e qualquer coisa definida no novo estará disponível.

*Podemos acessar definições através de namespaces, qualificando-as completamente* . Por exemplo, o espaço para nome _clojure.string_ define uma função _upper-case_.

Se estivermos no espaço de nomes clojure.string, podemos acessá-lo diretamente. Caso contrário, precisamos qualificá-lo como clojure.string/upper-case:

user=> (clojure.string/upper-case "hello")
"HELLO"
user=> (upper-case "hello") ; This is not visible in the "user" namespace
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: upper-case in this context
user=> (ns clojure.string)
nil
clojure.string=> (upper-case "hello") ; This is visible because we're now in the "clojure.string" namespace
"HELLO"
*Também podemos usar a palavra-chave _require_*  *para acessar definições de outro espaço para nome de uma maneira mais fácil* . Existem duas maneiras principais de usar isso - para definir um espaço para nome com um nome mais curto, para que seja mais fácil de usar, e para acessar definições de outro espaço para nome sem nenhum prefixo diretamente:
clojure.string=> (require '[clojure.string :as str])
nil
clojure.string=> (str/upper-case "Hello")
"HELLO"

user=> (require '[clojure.string :as str :refer [upper-case]])
nil
user=> (upper-case "Hello")
"HELLO"

Ambos afetam apenas o espaço para nome atual; portanto, mudar para um espaço diferente precisará ter novos requisitos. Isso ajuda a manter nossos espaços de nomes mais limpos e nos dá acesso apenas ao que precisamos.

* 4.4 Variáveis ​​*

*Depois que soubermos definir valores simples, podemos atribuí-los a variáveis.* Podemos fazer isso usando a palavra-chave _def_:
user=> (def a 123)
#'user/a
*Depois de fazer isso, podemos usar o símbolo _a_*  *em qualquer lugar que deseje representar esse valor:*
user=> a
123

Definições variáveis ​​podem ser tão simples ou complicadas quanto desejamos.

Por exemplo, para definir uma variável como a soma dos números, podemos fazer:

user=> (def b (+ 1 2 3 4 5))
#'user/b
user=> b
15

Observe que nunca precisamos declarar a variável ou indicar que tipo é. Clojure determina automaticamente tudo isso para nós.

Se tentarmos usar uma variável que não foi definida, obteremos um erro:

user=> unknown
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: unknown in this context
user=> (def c (+ 1 unknown))
Syntax error compiling at (REPL:1:8).
Unable to resolve symbol: unknown in this context

Observe que a saída da função def parece um pouco diferente da entrada. Definir uma variável a retorna uma sequência de ‘user/a. Isso ocorre porque o resultado é um símbolo e esse símbolo é definido no espaço para nome atual.

* 4.5 Funções *

Já vimos alguns exemplos de como chamar funções no Clojure. Criamos uma lista que começa com a função a ser chamada e, em seguida, todos os parâmetros.

Quando essa lista é avaliada, obtemos o valor de retorno da função. Por exemplo:

user=> (java.time.Instant/now)
#object[java.time.Instant 0x4b6690c0 "2019-01-15T07:54:01.516Z"]
user=> (java.time.Instant/parse "2019-01-15T07:55:00Z")
#object[java.time.Instant 0x6b8d96d9 "2019-01-15T07:55:00Z"]
user=> (java.time.OffsetDateTime/of 2019 01 15 7 56 0 0 java.time.ZoneOffset/UTC)
#object[java.time.OffsetDateTime 0xf80945f "2019-01-15T07:56Z"]

Também podemos aninhar chamadas para funções, pois quando queremos passar a saída de uma chamada de função como parâmetro para outro:

user=> (java.time.OffsetDateTime/of 2018 01 15 7 57 0 0 (java.time.ZoneOffset/ofHours -5))
#object[java.time.OffsetDateTime 0x1cdc4c27 "2018-01-15T07:57-05:00"]

Além disso,* também podemos definir nossas funções , se desejarmos. *As funções são criadas usando o comando fn :

user=> (fn [a b]
  (println "Adding numbers" a "and" b)
  (+ a b)
)
#object[user$eval165$fn__166 0x5644dc81 "[email protected]"]

Infelizmente, isso não atribui à função um nome que pode ser usado . Em vez disso, podemos definir um símbolo que represente essa função usando def, exatamente como vimos para variáveis:

user=> (def add
  (fn [a b]
    (println "Adding numbers" a "and" b)
    (+ a b)
  )
)
#'user/add

Agora que definimos essa função, podemos chamá-la da mesma forma que qualquer outra função:

user=> (add 1 2)
Adding numbers 1 and 2
3

Por conveniência, Clojure também nos permite usar defn para definir uma função com um nome de uma só vez .

Por exemplo:

user=> (defn sub [a b]
  (println "Subtracting" b "from" a)
  (- a b)
)
#'user/sub
user=> (sub 5 2)
Subtracting 2 from 5
3

4.6 Let e ​​Variáveis ​​Locais

*A chamada _def_ define um símbolo que é global para o espaço para nome atual* . Normalmente, isso não é o que se deseja ao executar código. Em vez disso, *Clojure oferece a chamada _let_ para definir variáveis ​​locais para um bloco* . Isso é especialmente útil ao usá-las dentro de funções, nas quais você não deseja que as variáveis ​​vazem para fora da função.

Por exemplo, poderíamos definir nossa subfunção:

user=> (defn sub [a b]
  (def result (- a b))
  (println "Result: " result)
  result
)
#'user/sub

No entanto, usar isso tem o seguinte efeito colateral inesperado:

user=> (sub 1 2)
Result:  -1
-1
user=> result ; Still visible outside of the function
-1

Em vez disso, vamos reescrevê-lo usando let:

user=> (defn sub [a b]
  (let [result (- a b)]
    (println "Result: " result)
    result
  )
)
#'user/sub
user=> (sub 1 2)
Result:  -1
-1
user=> result
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: result in this context

Desta vez, o símbolo result não é visível fora da função. Ou, de fato, fora do bloco let no qual foi usado.

*5. Coleções *

Até agora, estamos interagindo principalmente com valores simples. Vimos listas também, mas nada mais.* O Clojure possui um conjunto completo de coleções que podem ser usadas, consistindo em listas, vetores, mapas e conjuntos *:

  • Um vetor é uma lista ordenada de valores - qualquer valor arbitrário pode ser colocado em um vetor, incluindo outras coleções.

  • Um conjunto é uma coleção não ordenada de valores e nunca pode conter o mesmo valor mais de uma vez.

  • Um mapa é um conjunto simples de pares chave/valor. É muito comum usar palavras-chave como chaves em um mapa, mas podemos usar qualquer valor que desejar, incluindo outras coleções. *Uma lista é muito semelhante a um vetor. A diferença é semelhante à entre um ArrayList e um LinkedList em Java. Normalmente, um vetor é preferido, mas uma lista é melhor se queremos adicionar elementos ao início ou se queremos acessar apenas os elementos em ordem seqüencial.

====* 5.1. Construindo coleções *

A criação de cada uma delas pode ser feita usando uma notação abreviada ou uma chamada de função:

; Vector
user=> [1 2 3]
[1 2 3]
user=> (vector 1 2 3)
[1 2 3]

; List
user=> '(1 2 3)
(1 2 3)
user=> (list 1 2 3)
(1 2 3)

; Set
user=> #{1 2 3}
#{1 3 2}
user=> (hash-set 1 2 3)
#{1 3 2}

; Map
user=> {:a 1 :b 2}
{:a 1, :b 2}
user=> (hash-map :a 1 :b 2)
{:b 2, :a 1}

Observe que os exemplos Set e Map não retornam os valores na mesma ordem. Isso ocorre porque essas coleções são inerentemente desordenadas e o que vemos depende de como elas são representadas na memória.

Também podemos ver que a sintaxe para criar uma lista é muito semelhante à sintaxe padrão do Clojure para expressões.* Uma expressão Clojure é, de fato, uma lista que é avaliada *, enquanto o caractere de apóstrofo aqui indica que queremos a lista real de valores em vez de avaliá-la.

*Podemos, é claro, atribuir uma coleção a uma variável da mesma maneira que qualquer outro valor* . Também podemos usar uma coleção como chave ou valor dentro de outra coleção.

As listas são consideradas seq. Isso significa que a classe implementa a interface ISeq. Todas as outras coleções podem ser convertidas em um seq usando a função seq:

user=> (seq [1 2 3])
(1 2 3)
user=> (seq #{1 2 3})
(1 3 2)
user=> (seq {:a 1 2 3})
([:a 1] [2 3])

5.2 Acessando coleções

Depois de termos uma coleção, podemos interagir com ela para recuperar os valores novamente. Como podemos fazer isso depende um pouco da coleção em questão, pois cada uma delas tem semântica diferente.

*Os vetores são a única coleção que nos permite obter qualquer valor arbitrário por índice* . Isso é feito avaliando o vetor e o índice como uma expressão:
user=> (my-vector 2) ; [1 2 3]
3
*Podemos fazer o mesmo, usando a mesma sintaxe, para mapas também* :
user=> (my-map :b)
2

Também temos funções para acessar vetores e listas para obter o primeiro valor, o último valor e o restante da lista:

user=> (first my-vector)
1
user=> (last my-list)
3
user=> (next my-vector)
(2 3)

Os mapas têm funções adicionais para obter toda a lista de chaves e valores:

user=> (keys my-map)
(:a :b)
user=> (vals my-map)
(1 2)
*O único acesso real que temos a conjuntos é ver se um elemento específico é um membro.*

Parece muito semelhante ao acesso a qualquer outra coleção:

user=> (my-set 1)
1
user=> (my-set 5)
nil

5.3. Identificando coleções

Vimos que a maneira como acessamos uma coleção varia de acordo com o tipo de coleção que temos. Temos um conjunto de funções que podemos usar para determinar isso, de maneira específica e mais genérica.

Cada uma de nossas coleções possui uma função específica para determinar se um determinado valor é desse tipo - list? _ Para listas, _set? _ Para conjuntos e assim por diante. Além disso, existe _seq? _ Para determinar se um determinado valor é um _seq de qualquer tipo e _associative? _ Para determinar se um determinado valor permite acesso associativo de qualquer tipo - o que significa vetores e mapas:

user=> (vector? [1 2 3]) ; A vector is a vector
true
user=> (vector? #{1 2 3}) ; A set is not a vector
false
user=> (list? '(1 2 3)) ; A list is a list
true
user=> (list? [1 2 3]) ; A vector is not a list
false
user=> (map? {:a 1 :b 2}) ; A map is a map
true
user=> (map? #{1 2 3}) ; A set is not a map
false
user=> (seq? '(1 2 3)) ; A list is a seq
true
user=> (seq? [1 2 3]) ; A vector is not a seq
false
user=> (seq? (seq [1 2 3])) ; A vector can be converted into a seq
true
user=> (associative? {:a 1 :b 2}) ; A map is associative
true
user=> (associative? [1 2 3]) ; A vector is associative
true
user=> (associative? '(1 2 3)) ; A list is not associative
false

5.4 Mutação de coleções

  • No Clojure, como na maioria das linguagens funcionais, todas as coleções são imutáveis ​​*. Tudo o que fazemos para alterar uma coleção resulta em uma nova coleção sendo criada para representar as alterações. Isso pode trazer enormes benefícios de eficiência e significa que não há risco de efeitos colaterais acidentais.

No entanto, também precisamos ter cuidado para entender isso, caso contrário, as alterações esperadas em nossas coleções não ocorrerão.

*A adição de novos elementos a um vetor, lista ou conjunto é feita usando _conj_* . Isso funciona de maneira diferente em cada um desses casos, mas com a mesma intenção básica:
user=> (conj [1 2 3] 4) ; Adds to the end
[1 2 3 4]
user=> (conj '(1 2 3) 4) ; Adds to the beginning
(4 1 2 3)
user=> (conj #{1 2 3} 4) ; Unordered
#{1 4 3 2}
user=> (conj #{1 2 3} 3) ; Adding an already present entry does nothing
#{1 3 2}
*Também podemos remover entradas de um conjunto usando _disj_* . Observe que isso não funciona em uma lista ou vetor, porque eles são estritamente ordenados:
user=> (disj #{1 2 3} 2) ; Removes the entry
#{1 3}
user=> (disj #{1 2 3} 4) ; Does nothing because the entry wasn't present
#{1 3 2}
*A adição de novos elementos a um mapa é feita usando _assoc_. Também podemos remover entradas de um mapa usando _dissoc: _*
user=> (assoc {:a 1 :b 2} :c 3) ; Adds a new key
{:a 1, :b 2, :c 3}
user=> (assoc {:a 1 :b 2} :b 3) ; Updates an existing key
{:a 1, :b 3}
user=> (dissoc {:a 1 :b 2} :b) ; Removes an existing key
{:a 1}
user=> (dissoc {:a 1 :b 2} :c) ; Does nothing because the key wasn't present
{:a 1, :b 2}

5.5. Construções de programação funcional

Clojure é, em sua essência, uma linguagem de programação funcional. Isso significa que * temos acesso a muitos conceitos tradicionais de programação funcional - como map, filter, e * * _ reduzem. _ * *Geralmente, funcionam da mesma maneira que em outros idiomas *. A sintaxe exata pode ser um pouco diferente, no entanto.

Especificamente, essas funções geralmente assumem a função de aplicar como o primeiro argumento e a coleção a ser aplicada como o segundo argumento:

user=> (map inc [1 2 3]) ; Increment every value in the vector
(2 3 4)
user=> (map inc #{1 2 3}) ; Increment every value in the set
(2 4 3)

user=> (filter odd? [1 2 3 4 5]) ; Only return odd values
(1 3 5)
user=> (remove odd? [1 2 3 4 5]) ; Only return non-odd values
(2 4)

user=> (reduce + [1 2 3 4 5]) ; Add all of the values together, returning the sum
15

*6. Estruturas de controle *

Como em todas as linguagens de uso geral, o Clojure apresenta chamadas para estruturas de controle padrão, como condicionais e loops.

====* 6.1 Condicionais*

*Condicionais são tratados pela instrução _if_* . São necessários três parâmetros: um teste, um bloco para executar se o teste for _true_ e um bloco para executar se o teste for _false_. Cada um deles pode ser um valor simples ou uma lista padrão que será avaliada sob demanda:
user=> (if true 1 2)
1
user=> (if false 1 2)
2

Nosso teste pode ser tudo o que precisamos - não precisa ser um valor true/false. Também pode ser um bloco que é avaliado para fornecer o valor que precisamos:

user=> (if (> 1 2) "True" "False")
"False"

Todas as verificações padrão, incluindo _ =,>, _ e _ <_, podem ser usadas aqui. Também há um conjunto de predicados que podem ser usados ​​por vários outros motivos - já vimos alguns ao examinar coleções, por exemplo:

user=> (if (odd? 1) "1 is odd" "1 is even")
"1 is odd"

O teste pode retornar qualquer valor - não precisa ser apenas true ou false. No entanto, é considerado true se o valor for qualquer coisa, exceto false ou nil. Isso é diferente da maneira como o JavaScript funciona, onde há um grande conjunto de valores que são considerados "verdade-y", mas não true:

user=> (if 0 "True" "False")
"True"
user=> (if [] "True" "False")
"True"
user=> (if nil "True" "False")
"False"

6.2 Looping

*Nosso suporte funcional a coleções lida com grande parte do trabalho de loop* - em vez de escrever um loop sobre a coleção, usamos as funções padrão e deixamos o idioma fazer a iteração para nós.
*Fora isso, o loop é feito inteiramente usando a recursão* . Podemos escrever funções recursivas ou podemos usar as palavras-chave __loop __ e __recur __ para escrever um loop de estilo recursivo:
user=> (loop [accum [] i 0]
  (if (= i 10)
    accum
    (recur (conj accum i) (inc i))
  ))
[0 1 2 3 4 5 6 7 8 9]

A chamada loop inicia um bloco interno que é executado em todas as iterações e inicia configurando alguns parâmetros iniciais. A chamada recur volta a chamar o loop, fornecendo os próximos parâmetros a serem usados ​​na iteração. Se recur não for chamado, o loop será finalizado.

Nesse caso, fazemos um loop sempre que o valor de i não for igual a 10 e, assim que for igual a 10, retornaremos o vetor acumulado de números.

*7. Resumo *

*Este artigo apresentou uma introdução à linguagem de programação Clojure e mostra como a sintaxe funciona e algumas das coisas que você pode fazer com ela.* Este é apenas um nível introdutório e não vai ao fundo de tudo o que pode ser possível. feito com o idioma.

No entanto, por que não buscá-lo, tente e veja o que você pode fazer com ele.