January 19, 2015

binding-form e associação de valores em Clojure

Se você der uma lida na documentação do Clojure você vai acabar encontrando binding-form escrito em alguns lugares que permite você associar valores a símbolos, como no caso do let e do defn.

No Clojure, existe o conceito de forms, que se refere ao formato da estrutura de dados que aquela função ou macro aceita. Lembrando que código também é dado em Clojure, então formas podem se referir a forma da interface de um método ou macro.

Quando nos referimos a forma de associação, estamos falando da estrutura de dados que podemos utilizar para dar valores a símbolos em um contexto. Vamos utilizar o let como exemplo. Com ele, podemos introduzir um novo contexto com símbolos associados a valores, e ele possui a forma

(let [pares-de-associação...] expressões...)

Cada par de associação ira associar o primeiro elemento com o valor do segundo.

(let [par1 "valor1"
      par2 "valor2"]
  (str par1 " e " par2)) ;; # => "valor1 e valor2"

Essa é a forma mais simples de associação: utilizar um simbolo para um valor. A ideia de desestruturar um valor na hora da atribuição não é exclusiva do Clojure.

Mas existem outras formatos que podemos utilizar, chamadas de desestruturação.

Desestruturação de Listas

O primeiro exemplo que vou utilizar esta na desestruturação de listas. Podemos na hora de associar os valores, pegar elementos de dentro da lista diretamente.

Você pode inspecionar a estrutura de dado que será atribuída aos símbolos e relacionar partes dela ao invés de ter que chamar funções e fazer criar variáveis consecutivas.

(let [lista [1 2 3]
      primeiro (nth lista 0)
      segundo (nth lista 1)
      terceiro (nth lista 2)]
  (+ primeiro segundo terceiro)) ;; => 6

(let [lista [1 2 3]
      [primeiro segundo terceiro] lista]
  (+ primeiro segundo terceiro)) ;; => 6

Escrevendo um let assim, podemos explorar esse aspecto da associação em Clojure. Ao invés de tentar associar o valor da lista apenas para um símbolo, nos criamos uma outra forma, uma lista com símbolos na posição dos valores que queremos associar a eles.

Esse tipo de desestruturação indexada funciona para estruturas sequenciais, que respondem a nth, como Strings, Listas e Vetores.

(let [[x] "exemplo"
      [y] '(1 2 3)
      [z] [4 5 6]]
  [x y z]) ;; => [\e 1 4]

Para estruturas sequenciais, ainda temos mais algumas opções de formas para desestruturar. Podemos combinar a atribuição de valores posicionais e capturar o restante da estrutura em outro símbolo.

Trabalhando com listas, é bem comum ter interesse no primeiro elemento, e fazer uma recursão no resto da lista. Podemos utilizar a forma [a b c & rest] para pegar os valores restantes que não foram atribuídos por índice.

(let [lista [1 2 3]
      head (nth lista 0)
      tail (nthnext lista 1)]
  [head tail]) ;; => [1 [2 3]]

(let [lista [1 2 3]
      [head & tail] lista]
  [head tail]) ;; => [1 [2 3]]

E uma ultima opções que temos ao desestruturar sequências, é pegar o valor completo, mesmo depois de desestruturar, com a forma de [a b c :as tudo].

Podemos combinar os exemplos anteriores em uma única forma.

(let [[a b c & resto :as letras] "Ola Mundo"]
  [a b c resto letras]) ;; => [\O \l \a (\space \M \u \n \d \o) "Ola Mundo"]

Desestruturação de Mapas

Uma segunda estrutura de dados muito utilizada em Clojure são os Mapas. Da mesma forma que temos facilidades para trabalhar com valores dentro de Sequências, temos um binding-form para Mapas. Eles permitem desestruturação baseada na chave, para que você possa associar os símbolos valores direto no contexto.

Desta vez meus exemplos serão utilizando funções. Definir uma função no nível do namespace atual tem a seguinte forma.

(defn nome forma-de-associação expressões...)

A forma de associação será comparada contra os argumentos na hora de chamar a função, e tera os símbolos associados aos valores dentro do contexto das expressões.

(defn exemplo1 [mapa]
  (str
   " :chave => " (:chave mapa)
   " :nome => " (:nome mapa)))

(exemplo1 {:chave "abcd", :nome "Bruno"}) ;; => " :chave => abcd :nome => Bruno"

(defn exemplo2 [{chave :chave nome :nome}]
  (str
   " :chave => " chave
   " :nome => " nome))

(exemplo2 {:chave "abcd", :nome "Bruno"}) ;; => " :chave => abcd :nome => Bruno"

Cada um dos símbolos dentro da forma {símbolo chave símbolo chave} será associado com o valor da respectiva chave no Mapa.

As vezes, nos podemos querer um valor padrão quando uma chave não estiver presente no Mapa que estamos desestruturando. Com a adição de um :or na forma, nos podemos dar valores padrões aos símbolos que não foram associados a nenhum valor.

Ele passa a ter a seguinte forma, como você vera no exemplo.

{símbolo chave ... :or {símbolo valor símbolo valor}}

(defn exemplo1 [mapa]
  (str
   " :chave => " (:chave mapa "<sem chave>")
   " :nome => " (:nome mapa "<sem nome>")))

(exemplo1 {:chave "abcd", :nome "Bruno"}) ;; => " :chave => abcd :nome => Bruno"
(exemplo1 {:nome "Bruno"}) ;; => " :chave => <sem chave> :nome => Bruno"

(defn exemplo2 [{chave :chave
                 nome :nome
                 :or {chave "<sem chave>" nome "<sem nome>"}}]
  (str
   " :chave => " chave
   " :nome => " nome))

(exemplo2 {:chave "abcd", :nome "Bruno"}) ;; => " :chave => abcd :nome => Bruno"
(exemplo2 {:chave "abcd"}) ;; => " :chave => abcd :nome => <sem nome>"

Da mesma forma que fizemos nas Sequências, podemos atribuir o valor do Mapa inteiro utilizando :as enquanto estivermos desestruturando o valor.

(defn example [{a :a :as mapa}]
  [a mapa])

(example {:a 1 :b 2}) ;; => [1 {:a 1, :b 2}]

Muitas das vezes, quando trabalhando com mapas, nos utilizamos um símbolo como o mesmo nome que a chave do valor que buscamos. Para evitar digitar a mesma palavra, temos um atalho para buscar os valores de um mapa, de forma reflexiva.

O Clojure tem mais 3 opções para buscar valores na forma do Mapa: :keys, :strs e :syms. Esses atalhos recebem uma vetor de símbolos que serão convertidos para keywords, strings ou símbolos respectivamente, antes de fazer a consulta no mapa.

(let [mapa {:a 1 'b 2 "c" 3 :d 4}
      {:keys [a d]} mapa
      {:syms [b]} mapa
      {:strs [c]} mapa]
  [a b c d]) ;; => [1 2 3 4]

Nos podemos combinar todas essas opções para definir nossa forma de associação e desestruturar os valores.

Um ultimo ponto importante é que podemos combinar todas essas formas aninhadas. Podemos extrair o terceiro elemento de uma sequência associada a uma chave em um mapa utilizando o que vimos agora, por exemplo.

(def meu-mapa {:chave "abcd"
               :valores [1 2 3 4 5]})

(defn terceiro-valor
  [{[_ _ terceiro] :valores}]
  terceiro)

(terceiro-valor meu-mapa) ;; => 3

Exemplos de código

Utilizar os vários formatos de binding-form é uma pratica bem comum em projetos Clojure, e você pode conferir pelo Github como as pessoas costumam utilizar.

  • Extraindo valores de mapas - debug.clj
  • Pegar o primeiro grupo capturado por um regex - ping.clj
  • Valores de mapas dentro de mapas - compiler.clj

Com o tempo você vai começar a ver onde seu código ficaria melhor utilizando as desestruturações.

Mais links