Jeg så nylig foredraget Stop Writing Dead Programs fra Strange Loop 2022, og ble minnet på at en av de tingene jeg koser meg fælt med når jeg skriver Clojure er at det er en interaktiv prosess med et levende program. Det er Clojure sitt REPL som gjør dette mulig, og jeg skal nå forsøke å forklare hvorfor et REPL er det essensielle verktøyet du kanskje ikke visste at du mangler i (arbeids)livet ditt.
¶Hva er et REPL?
“REPL” står for Read-Eval-Print-Loop. Kort fortalt er det en prosess der du kan skrive inn litt kode, få den evaluert, og se resultatet printet ut. REPL ble først laget for Lisp på 60-tallet, men i dag har de aller fleste programmeringsspråk en eller annen tilnærming til denne prosessen. Men et godt REPL kan mer enn bare å lese, evaluere, og printe.
Hvis du har node.js installert på maskinen din kan du enkelt prøve et slikt rudimentært REPL fra terminalen din:
✗ node
Welcome to Node.js v18.2.0.
Type ".help" for more information.
> console.log("lol")
lol
Denne typen programmeringsspråk-prompt kan være nyttige for utforskende arbeid, men siden de baserer seg på at du kaster litt kode inn i et svart hull og får et svar tilbake, har de begrenset verdi for større oppgaver og gjenbrukbar kode. Det er mulig å få litt hjelp av shell-historikk og lignende, men savnet etter en persistent fil med kode melder seg fort.
¶Interaksjon med kjørende prosess
Noe som ligger mye nærmere det vi skal snakke om i resten av dette innlegget er devtools i nettleseren din. Der finner du et konsoll ala det node nettopp ga oss - det kan lese og evaluere kode. Men det stopper ikke der, for konsollet i nettleseren er en del av app-prosessen til frontenden din. Det betyr at du kan interagere med både kode og data som flyter gjennom appen din.
Eksempelvis kan en velplassert global variabel gi deg tilgang til data fra langt inne i frontend-koden din etter at du har trykket på noen knapper og navigert litt rundt:
function UserButton(user) {
return div({
className: 'button',
onClick(e) {
window.user = user;
// ...
}
}, user.name);
}
Etter at du har klikket på denne knappen vil user
være tilgjengelig for
inspeksjon i konsollet. Har du jobbet litt i dette konsollet har du nok også
kjent på at ergonomien setter en del begrensninger på hvor lenge du orker holde
på, og hvor mye du får til.
¶Interaksjon med REPL-et fra editoren
Ved å integrere REPL-et i editoren kan vi oppnå langt bedre ergonomi. Takket være sin Lisp-arv, følger det med et REPL når du starter en Clojure-prosess, som vi kan integrere i editoren. Dette gir oss et tilsvarende konsoll i editoren, men i stedet for å skrive kode her kan vi bruke editor-integrasjonen til å “sende til REPL-et”.
Herfra skal jeg snakke konkret om Emacs sine verktøyer for Clojure, men tilsvarende verktøy finnes også for andre editorer.
Når du sitter med noe kode i Emacs kan du når som helst bruke en tastekombinasjon til å sende uttrykket til venstre for cursoren til REPL-et for evaluering, og så printer Emacs svaret ut til høyre for cursoren din (eller i et eget vindu, med en dedikert kommando - for større datamengder).
Teksten til høyre forsvinner når jeg beveger cursoren. På denne måten har vi løst problemet med det svarte hullet: nå kan vi i stedet putte masse nyttige kodesnutter i en fil og jobbe med den over tid. Fantasien er eneste begrensning på hva som kan være nyttig å ha i en sånn fil: eksempler på API-kall, utforskende bruk av Clojure’s kjernebiblioteker, tredjepartsbiblioteker, eller egen kode. Hva som helst.
¶Interaksjon med kjørende prosess fra editoren
Siden REPL-et kjører som en del av app-prosessen kan vi interagere med den på samme måte som vi gjorde med frontend-koden i nettleseren. Og fordi REPL-et er integrert i editoren trenger vi heller ikke å jobbe med kompilert/prosessert kode. Det åpner opp for en interaktiv måte å programmere på som jeg ikke ville vært foruten.
Med editor-integrasjonen har vi beveget oss bort fra å skrive kode inn i et konsoll. I stedet sender vi kode fra kildefilene til REPL-prosessen. Det betyr at du kan endre på en funksjon i kodebasen din og sende den til REPL-et på samme måte som regnestykket over. Da vil koden bli kompilert, og den nye definisjonen tar den gamles plass.
(defn login-handler [req]
(let [result (auth/attempt-login req)]
(if (:success? result)
{:status 301
:headers {"location" "/"}}
{:status 401
:body "Oh no, you don't!"})))
Her har jeg en hypotetisk liten HTTP-handler som gjør login. La oss si at jeg driver og tester noe greier, og har lyst til å tvinge meg forbi loginen. Da kan jeg endre litt på koden, og sende til REPL-et (jeg trenger ikke en gang å lagre fila):
(defn login-handler [req]
(let [result (auth/attempt-login req)]
(if (or (= "christian" (-> req :params :username))
(:success? result))
{:status 301
:headers {"location" "/"}}
{:status 401
:body "Oh no, you don't!"})))
Sender jeg denne til REPL-et kan jeg enkelt og greit undergrave hele autentiseringen. Dette nivået av “REPL-ing” ligner “hot reloading” av kode. En viktig forskjell fra hot reloading er at jeg kun redefinerer denne ene funksjonen - tilstand som er bygget opp i prosessen vil ikke gå tapt, ei heller andre modifikasjoner jeg har gjort på samme vis.
Hvor er giffen min?
Da jeg skrev dette innlegget la jeg til en animert gif som illustrerte bruken av REPL-et. Den dukka ikke opp, først fordi blogg-systemet vårt ikke plukka opp gif-er fra disk, og deretter fordi jeg er en kløne. Under kan du se hvordan jeg løste problemet.
¶Interaksjon med prosessens data
Hot reloading er kanskje ikke så spennende, men hva med å kunne tukle litt med prosessens data? La oss si at jeg evaluerer denne versjonen av login-handleren min:
(defn login-handler [req]
(let [result (auth/attempt-login req)]
(def login-data {:req req :result result})
(if (:success? result)
{:status 301
:headers {"location" "/"}}
{:status 401
:body "Oh no you don't!"})))
Nå vil funksjonen min lage en variabel login-data
hver gang login-handler
kalles. For å gjøre det kan jeg ganske enkelt logge inn i nettleseren - REPL-et
er en del av den samme prosessen som er appen min. Etterpå kan jeg jobbe med
dataene som ble fanget opp i REPL-et, og jeg har hele appens tilstand og
bibliotek av funksjoner lett tilgjengelig. REPL-et gir meg altså toveis
interaksjon med den kjørende koden. Det er både ganske kult og svært nyttig!
Det er litt dumt å ha masse debuggingkode slengende rundt i produksjonskoden.
Jeg kan legge det på et eget sted, eller jeg kan bruke Clojure sin hendige
comment
:
(comment
;; Evaluer denne for å se på resultatet
(:result login-data)
;; Mener systemet at brukeren som logget inn er admin?
(auth/admin-user? (-> login-data :result :user))
)
Hva gjør comment
? Ingen ting! Kompilatoren stripper den ut, slik at dette ikke
blir med i produksjonsbygget. Men under utvikling har jeg koden rett i nærheten
av der den er nyttig, og kan evaluere den når det trengs.
Under ser du et eksempel der jeg roter rundt i Kodemaker-bloggens maskineri for
å bli klok på hvordan vi egentlig velger ut relevante bloggposter. Her bytter
jeg på et tidspunkt ut defn
med defn*
fra
snitch som eksponerer alle
argumenter og lokale vars fra et funksjonskall som globale variabler i
namespacet:
Hvis du er nysgjerrig på implementasjonen så ligger kildekoden som lager kodemaker.no åpent på github.
¶Permanente comments
I koden jeg jobber i daglig finnes det en comment
i de aller fleste filene vi
jobber i. I disse ligger det ymse relevante kodesnutter som vi kan evaluere for
å få innblikk i hvordan ting virker, eller for å kikke på litt data, prøve noen
funksjoner, undersøke saker fra kundesenteret, eller hva det nå skulle være.
Disse snuttene kan være enda nyttigere enn de vi så over. Våre gir eksempelvis tilgang til “systemet” - altså objektet som holder på alle appens prosesser - webserveren, databasetilkoblingen, køsystemet, sub-systemet for å sende meldinger, appens config, osv. Da kan man gjøre mye gøy.
Under følger et helt reellt eksempel fra kodebasen jeg sitter i på jobb. Ved å
hente tokenet mitt fra nettleseren og paste det inn som token
kan jeg kjøre
funksjonene som blir kalt av handlerne som frontenden vår kaller på.
(comment
(def context (:app/context integrant.repl.state/system))
(def token "...")
(def request
{:context context
:jwt (vite.service/decode-jwt token)})
(get-vehicle-data request)
(smart-charging/get-vehicle-vendors
context
(auth/get-requesting-user request))
(get-vehicle-data
{:context context
:jwt {:claims {:smart-charging {:user-id "a70e9989-9d09-...-....-............"}}}})
)
Disse blokkene går i git sammen med resten av koden og sparer oss for masse tid hver eneste dag, samtidig som de hjelper til å dokumentere koden på en mer interaktiv måte.
¶REPL som TDD-erstatning
Jeg var i sin tid svært begeistret for test-drevet utvikling på grunn av den gode feedback-loopen det gir (jeg liker fortsatt TDD til visse oppgaver, men det er en annen sak). REPL-drevet utvikling byr på en enda tettere feedback-loop enn TDD, og jeg bruker det ofte i stedet for, eller i kombinasjon med TDD.
Ettersom REPL-et lar deg enkelt evaluere kodesnutter, kan man starte med noe data og en linje kode, evaluere, bygge ut, evaluere igjen og jobbe iterativt til du har noe som kan flyttes over i en funksjon.
Under kan du se meg lene meg på REPL-et for å drive frem en funksjon som slår
sammen perioder dersom datoene overlapper - dato-mikkmakk er typisk kode jeg
kåler med, og hvor det er ekstra nyttig å få mye feedback underveis. Når du ser
“=>
” har jeg evaluert koden. Seansen varer noen minutter og kan gi deg et
inntrykk av hvordan denne iterative prosessen fungerer.
¶REPL i prod
REPL-et til Clojure kommer med en nettverksprotokoll, slik at du kan koble opp editoren din til en kjørende prosess. Det er ikke ofte jeg driver med dette, men når du har et problem som verken logger eller lokal debugging kan gi deg svar på, så er dette en superpower som kan redde deg.
Sist jeg kjørte et REPL mot prod så fant vi en bug i JVM-en som hadde med encoding av tekst å gjøre. Alt håret som ble revet ut til tross - dette problemet lot seg ikke reprodusere lokalt. Vi måtte rett og slett snappe opp verdiene i prod-prosessen for å konkludere. Uten et REPL i prosessen hadde vi nok aldri kommet til bunns i akkurat den saken.
¶REPL i levende live
Gleden med et godt REPL blir aller mest tydelig når du får jobba litt med det selv. Nest best er å se noen bruke det. Magnar og jeg lager screencasts både på norsk og engelsk der vi viser hvordan vi jobber med Clojure - og hvor REPL-et er en sentral stjerne i showet. Titt gjerne innom!
Avslutningsvis vil jeg nok en gang anbefale Stop Writing Dead Programs, og støtte oppfordringen: slutt å skriv døde programmer!