Ubuntu TechHive
save-tokens-and-build-with-data-driven-clarity-using-clojure-script-part.md
Save Tokens and Build with Data-Driven Clarity using Clojure(Script)
article.detalhe

Save Tokens and Build with Data-Driven Clarity using Clojure(Script)

reading.progresso 12 min de leitura

Economize Tokens e Construa com Clareza Orientada por Dados usando Clojure(Script)

História Rápida: Clojure(Script) e a Web

Do início dos anos 2000 em diante

  • Aprendi Vim e Emacs, mas era péssimo em ambos
  • HTTP e a Web
  • De LAMP a SPAs e mais Frameworks
  • Muita scriptagem e muita metaprogramação (JavaScript, Ruby, Groovy, Scala ...)
  • JavaScript passou de brinquedo a essencial

LISP

  • Muitos parênteses
  • Intrigante e estranho
  • Magos escreveram livros (Practical Common Lisp, SICP ...)
  • Ouvi muitas histórias loucas sobre isso (PG, HN, Hackers and Painters ...)
  • Várias implementações de LISP, todas muito estranhas

LISP Prático

  • Descobri Clojure em uma Conferência Java por volta de 2009
  • Finalmente algo relevante que valia a pena experimentar por causa da JVM
  • Primeira tentativa séria: Interface Java + Implementação Clojure em um aplicativo Spring
  • Melhorou: um novo Clojure sobre JavaScript (Google Closure Library, Google Closure Compiler: minificação agressiva de nível de produção)
  • Iluminação progressiva: (verb data)

Clojure(Script): Uma Ferramenta para Pensar com Clareza

  • REPL
  • A linguagem se adapta ao domínio
  • Sintaxe? Que sintaxe? É tudo estrutura de dados
  • Isomórfico
  • Clojure é uma linguagem hospedada (JVM, JavaScript, Dart e mais)
  • Chegue ao cerne da questão sem a bagunça
  • Especificações + funções + dados + validação: (specked-verb data)

Fundamentos de Clojure(Script)

Cada bloco de código abaixo foi escrito para que você possa colá-lo em um ClojureScript REPL como está durante uma demonstração ao vivo.
Na sintaxe Lisp, a primeira coisa após um parêntese de abertura é a função:

(+ 1 2 3)
;; => 6

(str "Hello, " "ClojureScript")
;; => "Hello, ClojureScript"

Literais de dados: valores primeiro

O código Clojure(Script) é feito principalmente de formas de dados simples:

42                         ;; número
"Ada"                      ;; string
:admin                     ;; palavra-chave, frequentemente usada como um rótulo de dados
[1 2 3]                    ;; vetor, valores ordenados
{:name "Ada" :visits 1}    ;; mapa, chaves para valores
#{:cljs :repl :data}        ;; conjunto, valores únicos

Namespaces e require

No REPL, use require para carregar um namespace e dar-lhe um alias curto.
Arquivos reais usam um formulário ns no topo, mas require é mais amigável para demonstrações ao vivo
no REPL.

(require ' [clojure.string :as str])

(str/upper-case "fast twitch")
;; => "FAST TWITCH"

(str/trim "  hello  ")
;; => "hello"

Keywords também podem funcionar como pequenas funções de pesquisa:

(def user {:name "Ada" :visits 1})

(:name user)
;; => "Ada"

(assoc user :active? true)
;; => {:name "Ada", :visits 1, :active? true}

(update user :visits inc)
;; => {:name "Ada", :visits 2}

Chaves e chaves com namespace

Uma chave de mapa é o rótulo usado para encontrar um valor. Keywords são chaves de mapa comuns
porque são pequenas, legíveis e funcionam como funções de pesquisa.

(def response
  {:status 200
   :body "ok"})

(get response :status)
;; => 200

(:body response)
;; => "ok"

Keywords com namespace adicionam um prefixo antes da barra. Eles ainda são apenas keywords, mas o prefixo ajuda a evitar colisões em mapas de dados maiores.

(def person
  {:person/name "Ada"
   :account/name "ada-lovelace"
   :account/id 42})

(:person/name person)
;; => "Ada"

(:account/name person)
;; => "ada-lovelace"

(select-keys person [:person/name :account/id])
;; => {:person/name "Ada", :account/id 42}

Você pode inspecionar a parte do namespace e a parte do nome de uma keyword:

(namespace :account/id)
;; => "account"

(name :account/id)
;; => "id"

(keyword "account" "email")
;; => :account/email

Funções

defn dá um nome a uma função. O vetor após o nome é a lista de argumentos.

(defn greet [name]
  (str "Hello, " name "!"))

(greet "Ada")
;; => "Hello, Ada!"

Funções anônimas são úteis quando a função é pequena e local:

((fn [x] (* x 10)) 7)
;; => 70

(#(* % 10) 7)
;; => 70

Funções de múltiplas aridades

Um nome de função pode suportar diferentes números de argumentos.

(defn greeting
  ([] (greeting "friend"))
  ([name] (str "Hello, " name))
  ([title name] (str "Hello, " title " " name)))

(greeting)
;; => "Hello, friend"

(greeting "Ada")
;; => "Hello, Ada"

(greeting "Dr." "Hopper")
;; => "Hello, Dr. Hopper"

Isso mantém padrões simples próximos da versão completa.

map

map executa uma função sobre cada item em uma coleção. Ele retorna uma sequência preguiçosa,
então use vec quando quiser ver um vetor no REPL.

(map inc [1 2 3])
;; => (2 3 4)

(vec (map inc [1 2 3]))
;; => [2 3 4]

(def todos
  [{:title "Read the schema" :done? true}
   {:title "Try the REPL" :done? false}])

(map :title todos)
;; => ("Read the schema" "Try the REPL")

->

A macro thread-first -> coloca o resultado anterior na próxima forma como o primeiro argumento de dados. Isso torna a limpeza de dados da esquerda para a direita mais fácil de ler.

(-> {:name "Ada" :visits 1}
    (assoc :active? true)
    (update :visits inc))
;; => {:name "Ada", :visits 2, :active? true}

Leia como: comece com este mapa, depois associe um valor, depois atualize um valor.

->>

A macro thread-last ->> coloca o resultado anterior no final da próxima forma. É comum para pipelines de coleções.

(def todos
  [{:title "Read the schema" :done? true}
   {:title "Try the REPL" :done? false}])

(->> todos
     (filter :done?)
     (map :title)
     vec)
;; => ["Read the schema"]

Leia como: pegue os "todos", mantenha os itens concluídos, pegue seus títulos, faça um vetor.

Nomes locais com let

let atribui nomes temporários a valores. Esses nomes só existem dentro do let.

(let [title "Try the REPL"
      done? false]
  {:title title
   :status (if done? "done" "open")})
;; => {:title "Try the REPL", :status "open"}

Desestruturação

A desestruturação extrai nomes de mapas e vetores sem chamadas manuais de get.

(def user {:name "Ada" :visits 1})

(let [{:keys [name visits]} user]
  (str name " visited " visits " time(s)"))
;; => "Ada visited 1 time(s)"

(let [ [first-item second-item & remaining] ["a" "b" "c" "d"]]
  {:first first-item
   :second second-item
   :remaining remaining})
;; => {:first "a", :second "b", :remaining ("c" "d")}

Isso é especialmente útil para mapas de requisição:

(def request
  {:request-method :get
   :params {:id "42"}
   :headers {"accept" "application/json"}})

(let [{:keys [request-method params]} request
      {:keys [id]} params]
  {:method request-method
   :todo-id id})
;; => {:method :get, :todo-id "42"}

if, when, if-let, e when-let

if escolhe entre dois valores. Apenas false e nil são falsos em
Clojure(Script); 0, "" e coleções vazias são verdadeiros.

(if (:done? {:title "Read" :done? true})
  "Done"
  "Open")
;; => "Done"

(if 0 "yes" "no")
;; => "yes"

when serve para "fazer isso apenas se o teste for verdadeiro"; caso contrário, retorna nil.

(when (seq "Ada")
  "There is a name")
;; => "There is a name"

(when (seq "")
  "This will not appear")
;; => nil

if-let e when-let vinculam um nome apenas quando o valor é verdadeiro. Este padrão
aparece frequentemente em correspondência de rotas, manipulação de cookies e renderização de navegador.

(def request-with-user {:session {:user "Ada"}})

(if-let [user (get-in request-with-user [:session :user])]
  (str "Signed in as " user)
  "Please log in")
;; => "Signed in as Ada"

(when-let [title (get-in {:params {:todo {:title "Ship demo"}}}
                         [:params :todo :title])]
  (str "Todo: " title))
;; => "Todo: Ship demo"

cond, case, and condp

Use cond quando você tem múltiplos testes. Ele os verifica em ordem.

(defn todo-status [{:keys [done? blocked?]}]
  (cond
    blocked? "blocked"
    done? "done"
    :else "open"))

(todo-status {:done? false :blocked? true})
;; => "blocked"

Use case quando um valor é comparado a constantes conhecidas.

(defn method-action [request-method]
  (case request-method
    :get "read"
    :post "write"
    :delete "remove"
    "unknown"))

(method-action :post)
;; => "write"

condp é menos comum neste repositório, mas útil quando cada ramificação usa o mesmo
predicado.

(defn score-band [score]
  (condp <= score
    90 :great
    75 :solid
    :needs-practice))

(score-band 82)
;; => :solid

cond->

cond-> começa com um valor e aplica cada passo apenas quando seu teste é verdadeiro.
O projeto usa isso para construir mapas de requisição, cabeçalhos e configuração de middleware.

(defn response-map [{:keys [body location]}]
  (cond-> {:status 200
           :headers {}}
    body
    (assoc :body body)

location
    (assoc-in [:headers "Location"] location)))

(response-map {:body "ok" :location "/todos"})
;; => {:status 200, :headers {"Location" "/todos"}, :body "ok"}

some->

some-> é como ->, mas ele para e retorna nil assim que um passo retorna
nil. Isso é útil quando dados podem estar faltando.

(some-> {:pathname {:groups {:id "42"}}}
        :pathname
        :groups
        :id)
;; => "42"

(some-> nil
        :pathname
        :groups
        :id)
;; => nil

Mapas aninhados: get-in, assoc-in, e update-in

Mapas de requisição e mapas de resposta são aninhados. Esses auxiliares evitam a busca manual.

(def response {:status 200 :headers {}})

(assoc-in response [:headers "Content-Type"] "text/plain")
;; => {:status 200, :headers {"Content-Type" "text/plain"}}

(get-in {:params {:todo {:title "Learn get-in"}}}
        [:params :todo :title])
;; => "Learn get-in"

(update-in {:todos {1 {:done? false}}}
           [:todos 1 :done?]
           not)
;; => {:todos {1 {:done? true}}}

Estado com atom, @, swap!, e reset!

A maioria dos dados é imutável. Quando o aplicativo precisa de estado mutável, este repositório usa
átomos, por exemplo, tarefas, contadores e estado da interface do usuário do navegador.

(def store
  (atom {:clicks 0
         :chips []}))

@store
;; => {:clicks 0, :chips []}

(swap! store update :clicks inc)
;; => {:clicks 1, :chips []}

(swap! store update :chips conj "REPL")
;; => {:clicks 1, :chips ["REPL"]}

(reset! store {:clicks 0 :chips []})
;; => {:clicks 0, :chips []}

Em arquivos-fonte, este repositório frequentemente usa defonce para estado do navegador ou do servidor.
defonce significa "defina isto apenas se ainda não existir", o que é útil
durante recarregamentos do REPL. Em uma demonstração de colagem ao vivo, um def simples é menos surpreendente
porque ele redefine o exemplo a cada vez.

reduce

reduce percorre uma coleção enquanto carrega um acumulador. A fonte o usa para construir mapas, cabeçalhos e configuração.

(reduce
 (fn [total n]
   (+ total n))
 0
 [10 20 30])
;; => 60

(reduce
 (fn [headers [k v]]
   (assoc headers k v))
 {}
 [ [:content-type "text/plain"]
  [:etag "v1"]])
;; => {:content-type "text/plain", :etag "v1"}

loop e recur

Use loop com recur quando precisar de um loop explícito sem contadores mutáveis. Em muitos casos, map, filter ou reduce serão mais simples.

(loop [n 3
       result []]
  (if (zero? n)
    result
    (recur (dec n) (conj result n))))
;; => [3 2 1]

Interoperabilidade com JavaScript

ClojureScript é hospedado em JavaScript, então podemos chamar JavaScript diretamente quando precisamos da plataforma.

(.log js/console "Hello from ClojureScript")
;; imprime no console JS

(.toISOString (js/Date.))
;; => hora atual como uma string ISO

(.round js/Math 4.6)
;; => 5


Use `clj->js` e `js->clj` nos limites da API:

```clojure
(def json-text
  (.stringify js/JSON (clj->js {:ok true :items [1 2 3]}) nil 2))

json-text
;; => "{\n  \"ok\": true,\n  \"items\": [\n    1,\n    2,\n    3\n  ]\n}"

(js->clj (.parse js/JSON json-text) :keywordize-keys true)
;; => {:ok true, :items [1 2 3]}

Para objetos JavaScript nativos, o acesso à propriedade é explícito:

(def js-user #js {:name "Ada" :visits 2})

(.-name js-user)
;; => "Ada"

(aget js-user "visits")
;; => 2

Promessas JavaScript

Muitas APIs de navegador, Node, Deno e Bun retornam promessas. Este repositório frequentemente usa
-> para tornar as cadeias de promessas legíveis.

(-> (js/Promise.resolve "hello")
    (.then (fn [text]
             (.toUpperCase text)))
    (.then (fn [loud]
             (.log js/console loud)))
    (.catch (fn [error]
              (.error js/console error))))
;; exibe "HELLO"; retorna uma Promise

try, catch e throw

Use try quando JavaScript ou código de análise puderem lançar erros. Em ClojureScript,
(catch :default error ...) captura erros normais de JavaScript.

(try
  (.parse js/JSON "{bad json")
  (catch :default error
    (.-message error)))
;; => uma mensagem de erro de análise JSON

(try
  (throw (js/Error. "Boom"))
  (catch :default error
    (str "Capturado: " (.-message error))))
;; => "Capturado: Boom"

Noções básicas de macros

Macros transformam código antes de ser executado. Já usamos macros: ->,
->>, when, cond->, e os auxiliares env-var / serve do repositório.

(macroexpand-1
 '(-> {:status 200}
      (assoc :body "ok")))
;; => (assoc {:status 200} :body "ok")

(macroexpand-1
 '(when true
    "runs when true"))
;; => (if true (do "runs when true"))

Neste repositório, macros personalizadas como env-var e serve vivem em src/fast_twitch/macros.cljc. Para uma demonstração ao vivo para iniciantes, comece com macroexpand-1 em macros principais antes de escrever macros personalizadas.

Assim que a ideia for compreendida, avalie o encadeamento condicional normalmente:

(cond-> {:status 200}
  true
  (assoc :body "ok")

false
  (assoc :debug true))
;; => {:status 200, :body "ok"}

Bibliotecas Úteis

Para os trechos de biblioteca, use um projeto ou exemplo que tenha a dependência no classpath. Neste repositório, examples/07-json-api-malli-openapi já possui Malli e Integrant.

Malli

Malli nos permite descrever dados com dados. Um esquema é apenas um valor Clojure(Script)
que podemos validar, explicar, transformar ou reutilizar para documentação.

(require ' [malli.core :as m])
(require ' [malli.error :as me])

(def TodoCreate
  [:map
   {:closed true}
   [:title [:string {:min 1}]]
   [:done {:optional true} :boolean]])

(m/validate TodoCreate {:title "Learn Malli" :done false})
;; => true

(m/validate TodoCreate {:title "" :extra "not allowed"})
;; => false

(me/humanize
 (m/explain TodoCreate {:title "" :extra "not allowed"}))
;; => {:title ["should be at least 1 character"],
;;     :extra ["disallowed key"]}

Use m/validator quando quiser uma função de validação reutilizável:

(require ' [malli.core :as m])

(def TodoCreate
  [:map
   {:closed true}
   [:title [:string {:min 1}]]
   [:done {:optional true} :boolean]])

(def valid-todo? (m/validator TodoCreate))

(valid-todo? {:title "Ship the demo"})
;; => true

(valid-todo? {:done true})
;; => false

A ideia importante para iniciantes: em vez de esconder regras de validação em muitas condicionais, colocamos a forma dos dados válidos em um pequeno valor de dados.

Integrant

Integrant inicia um aplicativo a partir de um mapa de configuração. Cada chave de nível superior descreve uma parte do sistema, e ig/ref indica que uma parte depende de outra.

(require ' [integrant.core :as ig])

(defmethod ig/init-key ::settings [_ options]
  options)

(defmethod ig/init-key ::greeter [_ {:keys [settings]}]
  (fn [name]
    (str (:prefix settings) ", " name "!")))

(def config
  {::settings {:prefix "Hello"}
   ::greeter {:settings (ig/ref ::settings)}})

(def system (ig/init config))

((::greeter system) "Ada")
;; => "Hello, Ada!"

O que aconteceu:

  • ::settings inicializado primeiro.
  • ::greeter recebeu as configurações inicializadas.
  • system é um mapa de partes inicializadas.

Adicione limpeza com ig/halt-key! para partes que possuem recursos:

(require ' [integrant.core :as ig])

(defmethod ig.init-key ::store [_ initial-state]
  (atom initial-state))

(defmethod ig.halt-key! ::store [_ store]
  (reset! store {}))

(def store-system
  (ig/init {::store {:todos []}}))

@(::store store-system)
;; => {:todos []}

(ig/halt! store-system)
;; => ignore the return value; halt! is for side effects

A ideia importante para iniciantes: mantenha a fiação como dados e mantenha o comportamento de inicialização e desligamento em pequenos métodos anexados a essas chaves de dados.

FastTwitch

Fonte: FastTwitch 🏃💨

  • Backend web sobre os principais runtimes JavaScript
  • Capturar o que é realmente necessário para fazer coisas de backend web
  • Várias tentativas
  • ExpressJs
  • Fastify
  • Agora apenas a API fetch e padrões web
  • API inspirada em ring