Clojure e simplicidade

AVISO – me empolguei um pouco nessa postagem, para uma introdução mais gentil, verifique o post após esse.

Qualquer linguagem baseada em LISP, como Clojure, tem o mesmo problema: as pessoas falam de como a linguagem é fantástica, como ela revoluciona como você programa, até o momento em que você resolve entender por que. Aí você estuda a linguagem, aprende uma ou outra coisa, e não entende porque as pessoas falam tão bem dela.

Esse ciclo se repete várias vezes, e várias vezes, e você nunca entende o motivo das pessoas falarem tão bem. Até um dia em que você finalmente entende – e é aí que você vira uma dessas pessoas que falam bem, mas ninguém mais entende por que.

Clojure é, basicamente, um LISP que roda sobre a JVM. Porém, diferente de common LISP, Clojure possui duck typing – LISP não. É essa foi a primeira realização – tipagem dinâmica não implica em duck typing.

Ruby, JS, e Clojure possuem métodos (ou keywords, ou funções) que rodam sobre qualquer tipo que atenda aquele protocolo. for element in array, por exemplo, roda em Ruby e JS da mesma maneira para Arrays, ou para Objects (em JS) ou Hash, Set, para Ruby. Em Ruby, é porque todos implementam o método .to_a. Já em Clojure, o nth serve para pegar um elemento de uma coleção qualquer, seja ela uma List ou Vector, usando (nth ["some" "elements"] 1). E como é isso em Common LISP? Bom, se for uma List, usa-se: (nth 1 '("some" "elements")). Se for um Vector, com (aref (vector "some" "elements") 1). E assim por diante (o que quer que isso signifique nessa situação, já que nem posicionamento dos parâmetros nem nome das funções é consistente).

A segunda coisa interessante de Clojure é a sua “sintaxe”, ou na verdade, ausência de sintaxe. Na prática, a sintaxe não existe – você programa definido diretamente as S-Expressions, como se fosse uma lista de comandos. Por exemplo:

; uma definição de uma função
(defn sum-ages [people]
  (reduce + (map :age people)))

; uma definição de uma lista
`(defn sum-ages [people]
   (reduce + (map :age people)))

A segunda expressão, apenas pela presença de um “quote”, torna-se uma lista. O primeiro e segundo elementos são Symbol, o terceiro elemento é um Vector que contém outro Symbol, e o quarto elemento é outra List: (reduce + (map :age people)), e assim as coisas continuam. Symbols, em Clojure, serão convertidos em sintaxe mais cedo ou mais tarde, então defn será clojure.core/defn, e chamará a função, símbolo, ou special-form desse nome mais cedo ou mais tarde. E isso é uma coisa fantástica pelos motivos que veremos a seguir. Mas o primeiro deles é bem óbvio: você não tem códigos – apenas dados. E como a linguagem é composta de dados, podemos manipulá-la, moldá-la, e alterá-la com macros. Além disso, Clojure é uma linguagem muito simples – ao contrário por exemplo, de Ruby, aonde a linguagem é complexa, mas programar nela é simples, em Clojure a linguagem é simples, mas programar nela é um pouco mais complicado.

E o motivo, por mais absurdo que pareça, é que nós, programadores, aprendemos a programar de forma errada

Eu já mencionei num post anterior que a programação é um exercício de abstração, e que abstraímos o mundo da mesma forma que ele funciona – com mutabilidade, e como isso se torna imprevisível. Também, num post futuro, vou mostrar como React, Clojure, Reagent, e outras coisas permitem um exercício interessante de abstração até mesmo para interfaces gráficas notavelmente chatas de se fazer, como HTML + CSS + Backend. Porém, sendo mais geral, temos uma dificuldade muito grande de pensar de forma simples no mundo. Isso é verdade também com linguagens: antes de aprendermos a programar algo já estamos pensando em como vamos representar, quais classes, objetos, qual será o tipo de método, atributos… quando na verdade, é tudo mais simples.

Para começar, vamos pensar numa funcionalidade: digamos que temos uma lista de registros de algum lugar (banco de dados, de uma API externa, um provider, não importa nesse momento). Para esse projeto, precisamos contar homônimos => pessoas que tem exatamente o mesmo nome. Digamos que para facilitar nossa vida, temos uma função chamada normalize, que normaliza os nomes – remove acentos, espaços duplos, etc (então, "MAURÍCIO SZABO" viraria "MAURICIO SZABO", sem o acento e sem os espaços a mais). Como fazer? Bom, em Ruby, criaríamos um mapa com todos os usuários, talvez usando group-by, e então chamando “count” nos valores. Exceto que essa não é uma abordagem muito boa, pois vai criar objetos intermediários: um “Hash” com os valores agrupados, e o nosso “Hash” final. Por causa disso, normalmente pensamos na seguinte abordagem: criar um hash vazio, e ir inserindo elementos à medida que é necessário.

Em Clojure, a maioria das operações que iteram sobre as coleções retornam lazy-seq – algo que se comporta como uma lista, mas cujos valores não são computados até que sejam necessários (exceto, claro, funções como reduce, group-by, etc). Então, em Clojure, um código desses seria “lazy”, e poderíamos representá-lo da seguinte maneira: dada uma lista de elementos (que contém “name” e “sn”, ou “surname”), agrupamos o nome com o sobrenome, normalizamos, e contamos a frequencia que eles aparecem. Isso é fácil:

; Dada uma lista como:
(def users [{:name "Mauricio" :sn "Szabo"}
            {:name "MauriCIO" :sn "Szabo"}
            {:name "Andre" :sn "Steil"}
            {:name "AndrE  " :sn "STEIL"}
            {:name "Mauricio" :sn "Santos"}])

; Aplicamos:
(defn list-of-users [users]
  (frequencies (map #(normalize (str (:name %) " " (:sn %))) users)))

Porém, essa não é a “maneira certa” de fazer. O ponto é que, na orientação a objetos, temos a tendência encapsular tudo. Qualquer sequencia de código é vista como passível de encapsulamento – se a idéia é rodar X, Y, e depois Z, vai ter um método que faz isso tudo. Na programação funcional, e em Clojure, isso acaba sendo um encapsulamento desnecessário. Se quisermos um código bem genérico, podemos criar um código que, recebendo uma lista de atributos, normalize o nome de um usuário. Aí, podemos compor do jeito que acharmos melhor:

(defn gen-key [name & keys]
  (let [to-key (apply juxt keys)]
    (map normalize (to-key name))))

; O código acima pode ser usado para gerar uma chave
; única a partir de qualquer coisa. Por exemplo:
(frequencies (map #(gen-key % :name :sn) users))

; Claro que assim, o nome e o sobrenome vem numa lista.
; Podemos aplicar outro map para juntá-los:
(frequencies 
  (map #(apply str (interpose " " %)) 
       (map #(gen-key % :name :sn) users)))

; Ou usar o "thread-last operator":
(->> users
     (map #(gen-key % :name :sn))
     (map #(apply str (interpose " " %)))
     frequencies)

O código acima é inclusive mais flexível, e pode ser usado em praticamente qualquer coisa. Ele é paralelizável, se for necessário, usando os reducers de Clojure. E ele pode ser usado para contar quantos registros tem em qualquer atributo de qualquer lista. Por exemplo, se for para contar o número de usuários com a mesma idade: (frequencies (map #(gen-key % :age) users)). Se for para AGRUPAR os usuários por sobrenome e idade: (group-by #(gen-key % :age :sn) users). A regra aqui: é simples montar a contagem e o agrupamento, então a gente encapsula outras coisas. Encapsulamento mais “leve” dá mais espaço para reaproveitamento do código.

Ah, esses parênteses…

Um dos maiores desafios de aprender Clojure são os parênteses. Eles funcionam como uma parede entre o usuário e a linguagem – mas, ao passar essa parede, esse grande obstáculo sintático, tudo fica fácil. Além disso, há uma vantagem nos parênteses – em Clojure, certos problemas como “precedência de operadores” simplesmente não existem. Sempre sabemos a precedência, e sempre sabemos o que estamos fazendo.

Por exemplo: o código em Ruby:

puts users.map do |u|
  u.login.upcase
end

Parece um código super inofensivo. I problema é que ele não roda – a precedência do do é menor do que a do puts. Em Clojure, novamente, isso não existe – se esse código fosse ser escrito em Clojure, ele seria:

(puts (map users #(upcase (login %))))

Claro que em Clojure, não temos a função upcase, login não se busca assim e, principalmente, o map primeiro recebe a função depois os elementos aonde ele vai rodar. Mas a idéia geral é essa – rodamos map, que recebe dois parâmetros – a lista a ser consultada, e uma função, que retorna um resultado que então é passado para puts. Sem surpresas.

Claro que a maioria dos programadores Clojure usam algum elemento para auxiliar nos parênteses. A maioria usa o paredit, que fecha os parênteses e garante que o arquivo estará sempre balanceado, e há os que usam o parinfer (como eu), que infere a posição dos parênteses a partir da indentação. Mais sobre isso mais tarde…

Outra imensa vantagem, além da óbvia de ser muito simples de se usar e se criar macros, é que não há elementos de sintaxe estranhos – por exemplo, em Ruby, você pode nomear um método com ponto de interrogação, mas somente um método – não uma variável. O if precisa de um then se ele usar só uma linha. Clojure não tem nada disso – a única coisa a se lembrar é a posição de cada parâmetro. Outra vantagem é que cada abertura de parênteses deixa claro: isso é uma função, e eu estou chamando-a com esses parâmetros. Novamente, sem surpresas – principle of less surprise ao extremo.

Um dos casos mais notáveis é o if do Clojure, comprando com os ternário de outras linguagens. Basicamente, um ternário é um quick if, com sintaxe mais estranha. Porém, comparando o número de caracteres:

(if (= a 10) "foo" "bar") 
a == 10 ? "foo" : "bar"

Apenas dois caracteres a menos. Ou seja, os parênteses parecem algo assustador no começo. Na verdade, eles são elementos simples, que determinam uma previsibilidade absurda do que está acontecendo, que até diminuem o número de caracteres a serem escritos, e com um auxiliador como o Parinfer, até garantem que a indentação está correta também.

Indentação

A indentação em Clojure é meio estranha – todo código é indentado dois espaços além do seu pai. Parece simples, mas quando lembramos que as variáveis locais (ou os let, para quem programa em Clojure já) costumam definir funções também, aí as coisas começam a ficar um pouco complicadas:

(defn factorial [x]
  (let [aux (fn [x, acc]
              (if (zero? x)
                acc
                (recur (dec x) (* x acc)))]
    (aux x 1)))

Porém, escrever tal código com o parinfer facilita tudo:

Fatorial com Parinfer

Simplicidade

Clojure não tem orientação a objetos. Ele suporta protocolos, records, e outras coisas que se comportam como objetos dentro da JVM, mas na prática, não são objetos no sentido literal.

Clojure é extensível com macros – usando construções da própria linguagem, você consegue entender sintaxe, semântica, e simular coisas. Por exemplo, o or na JVM é implementado a partir do if. Isso significa na prática que não é complicado escrever um compilador de Clojure para outras plataformas – e aí entra o ClojureScript.

Ganhamos no ClojureScript justamente porque podemos implementar um código no servidor, e acessá-lo do lado do browser. Isso significa que todas as estruturas de dados geradas pelo servidor vem para o browser também, e são tratadas com exatamente o mesmo código. E aí entra a extensão .cljc.

Arquivos cljc são compilados por todas as plataformas especificadas em seu arquivo de build. Isso significa que, se eu quiser fazer uma determinada validação no servidor e no cliente, eu uso o mesmo código para ambos. Se eu quero exibir, tratar, ou de alguma forma criar uma camada entre o dado cru que vem de um banco de dados e o que será exibido na tela, eu escrevo esse código em meu arquivo cljc e ele é entendido por ambos os lados. Mesmo código, mesma API, mesmas regras – basicamente, a promessa que node.js nos deu, mas nunca conseguiu cumprir plenamente.

Ainda há muito o que se falar de Clojure. Uma das coisas mais notáveis é a facilidade de processar listas, e o fato de que HTML é, basicamente, uma lista (integrações como a biblioteca React, e outras abordagens bem interessantes, vou falar num outro post), web pages funcionais, servidores funcionais, etc. Há muito o que discorrer, mas o ecossistema dessa linguagem nova já está bem maduro.

Por fim, uma coisa bem interessante (embora eu ainda tenha minha posição meio contra o tipo de desenvolvimento) é que Clojure tenta implementar bem o chamado “null pattern” – é esperado que nulos sejam passados em certas circunstâncias. Por exemplo, em Ruby, é comum código como:

user && user.system && user.system.host_info && 
  user.system.host_info.protocol
# Ou, no Rails:
user.try(:system).try(:host_info).try(:protocol)

Enquanto em Clojure:

(get-in user [:system :host-info :protocol]) ; => nil
;; Até "set" pode ser feito assim:
(assoc-in user [:system :host-info :protocol] "https")
; se user for nulo, o resultado será:
; {:system {:host-info {:protocol "https"}}}

; se user tiver informações, mas não system, o resultado será:
; {:login "foo" :name "bar" :system {:host-info {:protocol "https"}}}
; Etc..

E isso sim é bem notável, já que é um padrão que usamos muitas vezes em muitas situações diferentes.

Advertisements
This entry was posted in Clojure and tagged , , , , . Bookmark the permalink.

One Response to Clojure e simplicidade

  1. Pingback: Clojure, gentilmente | Maurício Szabo

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s