Historia Rápida: Clojure(Script) y la Web
Desde principios de los 2000 y más allá
- Aprendí Vim y Emacs pero era malo en ambos
- HTTP y la Web
- De LAMP a SPAs y más Frameworks
- Mucho scripting y mucha metaprogramación (JavaScript, Ruby, Groovy, Scala ...)
- JavaScript pasó de ser un juguete a ser fundamental
LISP
- Muchos paréntesis
- Intrigante y extraño
- Magos escribieron libros (Practical Common Lisp, SICP ...)
- Escuché muchas historias locas al respecto (PG, HN, Hackers and Painters ...)
- Varias implementaciones de LISP, todas muy extrañas
LISP Práctico
- Descubrí Clojure en una Conferencia Java alrededor de 2009
- Finalmente algo relevante que valía la pena probar gracias a la JVM
- Primer intento serio: Interfaz Java + Implementación Clojure en una aplicación Spring
- Mejoró: un nuevo Clojure sobre JavaScript (Google Closure Library, Google Closure Compiler: minificación agresiva de grado de producción)
- Iluminación progresiva: (verbo datos)
Clojure(Script): Una Herramienta para Pensar con Claridad
- REPL
- El lenguaje se adapta al dominio
- ¿Sintaxis? ¿Qué sintaxis? Todo son estructuras de datos
- Isomórfico
- Clojure es un lenguaje alojado (JVM, JavaScript, Dart y más)
- Ir al grano sin el desorden
- Especificaciones + funciones + datos + validación: (verbo-especificado datos)
Fundamentos de Clojure(Script)
Cada bloque de código a continuación está escrito para que pueda pegarlo en un ClojureScript REPL tal cual durante una demostración en vivo.
En la sintaxis Lisp, lo primero después de un paréntesis de apertura es la función:
(+ 1 2 3)
;; => 6
(str "Hello, " "ClojureScript")
;; => "Hello, ClojureScript"
Literales de datos: valores primero
El código de Clojure(Script) se compone principalmente de formas de datos simples:
42 ;; número
"Ada" ;; cadena
:admin ;; palabra clave, a menudo usada como etiqueta de datos
[1 2 3] ;; vector, valores ordenados
{:name "Ada" :visits 1} ;; mapa, claves a valores
#{:cljs :repl :data} ;; conjunto, valores únicos
Espacios de nombres y require
En el REPL, usa require para cargar un espacio de nombres y asignarle un alias corto.
Los archivos reales usan una forma ns en la parte superior, pero require es más amigable para demostraciones en vivo del REPL.
(require ' [clojure.string :as str])
(str/upper-case "fast twitch")
;; => "FAST TWITCH"
(str/trim " hello ")
;; => "hello"
Las palabras clave también pueden actuar como pequeñas funciones de búsqueda:
(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}
Claves y claves con espacio de nombres
Una clave de mapa es la etiqueta utilizada para encontrar un valor. Las palabras clave son claves de mapa comunes
porque son pequeñas, legibles y funcionan como funciones de búsqueda.
(def response
{:status 200
:body "ok"})
(get response :status)
;; => 200
(:body response)
;; => "ok"
Las palabras clave con espacio de nombres añaden un prefijo antes de la barra. Siguen siendo solo palabras clave, pero el prefijo ayuda a evitar colisiones en mapas de datos más grandes.
(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}
Puedes inspeccionar la parte del espacio de nombres y la parte del nombre de una palabra clave:
(namespace :account/id)
;; => "account"
(name :account/id)
;; => "id"
(keyword "account" "email")
;; => :account/email
Funciones
defn le da un nombre a una función. El vector después del nombre es la lista de argumentos.
(defn greet [name]
(str "Hello, " name "!"))
(greet "Ada")
;; => "Hello, Ada!"
Las funciones anónimas son útiles cuando la función es pequeña y local:
((fn [x] (* x 10)) 7)
;; => 70
(#(* % 10) 7)
;; => 70
Funciones de aridad múltiple
Un nombre de función puede admitir 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"
Esto mantiene los valores predeterminados simples cerca de la versión completa.
map
map ejecuta una función sobre cada elemento de una colección. Devuelve una secuencia perezosa,
así que usa vec cuando quieras ver un vector en el 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")
->
La macro thread-first -> coloca el resultado anterior en la siguiente forma como el primer argumento de datos. Facilita la lectura de la limpieza de datos de izquierda a derecha.
(-> {:name "Ada" :visits 1}
(assoc :active? true)
(update :visits inc))
;; => {:name "Ada", :visits 2, :active? true}
Léase como: empieza con este mapa, luego asocia un valor, luego actualiza un valor.
->>
La macro thread-last ->> coloca el resultado anterior al final de la siguiente forma. Es común para las tuberías de colecciones.
(def todos
[{:title "Read the schema" :done? true}
{:title "Try the REPL" :done? false}])
(->> todos
(filter :done?)
(map :title)
vec)
;; => ["Read the schema"]
Léase como: toma los "todos", conserva los elementos terminados, toma sus títulos, haz un vector.
Nombres locales con let
let asigna nombres temporales a los valores. Estos nombres solo existen dentro del let.
(let [title "Try the REPL"
done? false]
{:title title
:status (if done? "done" "open")})
;; => {:title "Try the REPL", :status "open"}
Desestructuración
La desestructuración extrae nombres de mapas y vectores sin llamadas manuales a 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")}
Esto es especialmente útil para los mapas de solicitud:
(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`, y `when-let`
`if` elige entre dos valores. Solo `false` y `nil` son falsos en
Clojure(Script); `0`, `""` y las colecciones vacías son verdaderas.
```clojure
(if (:done? {:title "Read" :done? true})
"Done"
"Open")
;; => "Done"
(if 0 "yes" "no")
;; => "yes"
when es para "haz esto solo si la prueba es verdadera"; de lo contrario, devuelve nil.
(when (seq "Ada")
"There is a name")
;; => "There is a name"
(when (seq "")
"This will not appear")
;; => nil
if-let y when-let vinculan un nombre solo cuando el valor es verdadero. Este patrón
aparece a menudo en la coincidencia de rutas, el manejo de cookies y la representación del 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, y condp
Use cond cuando tenga múltiples pruebas. Las verifica en orden.
(defn todo-status [{:keys [done? blocked?]}]
(cond
blocked? "blocked"
done? "done"
:else "open"))
(todo-status {:done? false :blocked? true})
;; => "blocked"
Use case cuando un valor se compara con constantes conocidas.
(defn method-action [request-method]
(case request-method
:get "read"
:post "write"
:delete "remove"
"unknown"))
(method-action :post)
;; => "write"
condp es menos común en este repositorio, pero útil cuando cada rama usa el mismo
predicado.
(defn score-band [score]
(condp <= score
90 :great
75 :solid
:needs-practice))
(score-band 82)
;; => :solid
cond->
cond-> comienza con un valor y aplica cada paso solo cuando su prueba es verdadera.
El proyecto lo usa para construir mapas de solicitud, encabezados y configuración 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-> es como ->, pero se detiene y devuelve nil tan pronto como un paso devuelve
nil. Esto es útil cuando los datos pueden faltar.
(some-> {:pathname {:groups {:id "42"}}}
:pathname
:groups
:id)
;; => "42"
(some-> nil
:pathname
:groups
:id)
;; => nil
Mapas anidados: get-in, assoc-in, y update-in
Los mapas de solicitud y los mapas de respuesta están anidados. Estas funciones auxiliares evitan la búsqueda 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 con atom, @, swap!, y reset!
La mayoría de los datos son inmutables. Cuando la aplicación necesita cambiar de estado, este repositorio utiliza
átomos, por ejemplo, tareas pendientes, contadores y el estado de la interfaz de usuario del 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 []}
En los archivos fuente, este repositorio a menudo usa defonce para el estado del navegador o del servidor.
defonce significa "definir esto solo si aún no existe", lo cual es útil
durante las recargas de REPL. En una demostración en vivo, un def simple es menos sorprendente
porque reinicia el ejemplo cada vez.
reduce
reduce recorre una colección mientras lleva un acumulador. La fuente lo utiliza para construir mapas, encabezados y configuración.
(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 y recur
Usa loop con recur cuando necesites un bucle explícito sin contadores mutables.
En muchos casos, map, filter o reduce serán más simples.
(loop [n 3
result []]
(if (zero? n)
result
(recur (dec n) (conj result n))))
;; => [3 2 1]
Interoperabilidad con JavaScript
ClojureScript se aloja en JavaScript, por lo que podemos llamar a JavaScript directamente cuando
necesitemos la plataforma.
(.log js/console "Hello from ClojureScript")
;; imprime en la consola de JS
(.toISOString (js/Date.))
;; => hora actual como una cadena ISO
(.round js/Math 4.6)
;; => 5
Usa `clj->js` y `js->clj` en los límites de la 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 nativos de JavaScript, el acceso a propiedades es explícito:
(def js-user #js {:name "Ada" :visits 2})
(.-name js-user)
;; => "Ada"
(aget js-user "visits")
;; => 2
Promesas de JavaScript
Muchas APIs de navegador, Node, Deno y Bun devuelven promesas. Este repositorio a menudo usa
-> para hacer que las cadenas de promesas sean legibles.
(-> (js/Promise.resolve "hello")
(.then (fn [text]
(.toUpperCase text)))
(.then (fn [loud]
(.log js/console loud)))
(.catch (fn [error]
(.error js/console error))))
;; registra "HELLO"; devuelve una Promesa
try, catch, y throw
Usa try cuando JavaScript o el código de análisis puedan lanzar un error. En ClojureScript,
(catch :default error ...) captura errores normales de JavaScript.
(try
(.parse js/JSON "{bad json")
(catch :default error
(.-message error)))
;; => un mensaje de error de análisis JSON
(try
(throw (js/Error. "Boom"))
(catch :default error
(str "Caught: " (.-message error))))
;; => "Capturado: Boom"
Conceptos básicos de macros
Las macros transforman el código antes de que se ejecute. Ya hemos usado macros: ->,
->>, when, cond->, y los ayudantes env-var / serve del repositorio.
(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"))
En este repositorio, las macros personalizadas como env-var y serve residen en
src/fast_twitch/macros.cljc. Para una demostración en vivo para principiantes, comienza con
macroexpand-1 en las macros principales antes de escribir macros personalizadas.
Una vez que la idea se asimila, evalúa el encadenamiento condicional normalmente:
(cond-> {:status 200}
true
(assoc :body "ok")
false
(assoc :debug true))
;; => {:status 200, :body "ok"}
Librerías Útiles
Para los fragmentos de librería, usa un proyecto o ejemplo que tenga la dependencia en
el classpath. En este repositorio, examples/07-json-api-malli-openapi ya tiene
tanto Malli como Integrant.
Malli
Malli nos permite describir datos con datos. Un esquema es simplemente un valor de Clojure(Script)
que podemos validar, explicar, transformar o reutilizar para la documentación.
(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"]}
Usa m/validator cuando quieras una función de validación reutilizable:
(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
La idea importante para los principiantes: en lugar de ocultar las reglas de validación en muchos
condicionales, ponemos la forma de los datos válidos en un pequeño valor de datos.
Integrant
Integrant inicia una aplicación a partir de un mapa de configuración. Cada clave de nivel superior describe una
parte del sistema, y ig/ref indica que una parte depende de otra.
(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!"
Lo que pasó:
::settingsinicializado primero.::greeterrecibió la configuración inicializada.systemes un mapa de partes inicializadas.
Añade limpieza con ig/halt-key! para las partes que poseen 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
La idea importante para principiantes: mantén el cableado como datos, y mantén el comportamiento de inicio y apagado en pequeños métodos adjuntos a esas claves de datos.
FastTwitch
Fuente: FastTwitch 🏃💨
- Backend web sobre los principales tiempos de ejecución de JavaScript
- Capturar lo que realmente se necesita para hacer cosas de backend web
- Varios intentos
- ExpressJs
- Fastify
- Ahora solo la API
fetchy los estándares web - API inspirada en
ring
