Mikrobloggeriet

Unngå forgrening!

Jeg har blitt mye mer var på når jeg innfører forgrening i kode enn før. Men hva i all verden skal det egentlig bety?

Å “innføre en gren” betyr noe mer enn å kjøre git switch --create, ikke sant?

De første grenene springer ut fra trestammen

Ordet “branch” på norsk betyr “gren”. Trær har grener.

Vachellia erioloba er et tre, bilde fra Wikimedia (CC-BY-SA 4.0), https://en.wikipedia.org/wiki/Branch#/media/File:Kameldornbaum_Sossusvlei.jpg

Et tre har én trestamme. Når trestammen først forgrener seg, får vi grener ut fra trestammen.

Trestammen forgrener seg og blir til flere grener. Grener kan også forgrene seg i flere grener.

Hver if lager en ny gren i koden

Før hadde denne koden én logisk flyt. Nå har den to!

Hver gang koden skal feilsøkes, må vi skjønne begge grenene i koden. Kunne vi latt være å forgrene? Når vi forgrener fra nye grener gjør vi det enda vanskeligere for oss selv.

Jeg liker å forgrene i starten, og deretter la koden flyte så rett som mulig.

Hver git switch --create lager en ny gren i hodet til utviklerne

Vi stopper ikke alltid ved skillet mellom lokal kode og produksjon. Git kan holde styr på historikken til koden. Git kan også la oss lage så mange grener vi ønsker. Det er ingen grenser! Jo flere grener vi lager, jo flere grener må vi kjenne forskjellene mellom. Og når vi skal slå sammen to grener (anti-forgrene?), får vi alle forskjellene tilbake i fleisen.

Hvis vi lar være å forgrene, slipper vi å holde styr på alle grenene.

Tidlig verdi, høy kohesjon og lav kobling: tre lekser fra Mikrobloggeriet

Morn :) Teodor her. Å jobbe med Mikrobloggeriet har vært en læringsreise for meg. Fra starten ønsket jeg å lage en arena der vi kan lære sammen og være nysgjerrige; der selve systemet også er formbart og inkluderende. Per 2024-12-21 teller Mikrobloggeriet 1669 commits, og om lag 100 publiserte mikroblogginnlegg (som får meg til å tenke at en statistikkside hadde vært gøy).

Snøen faller i Oslo, og kanskje roen vil senke seg etter hvert. Å roe ned for meg handler ofte om refleksjon og langsiktighet. Når stresset letter litt og antallet ting som skal gjøres hver dag faller, kommer motivasjonen til å se litt tilbake og å se litt fram snikende.

Jeg vil gå gjennom tre ting jeg mener har vært viktig, hvorfor det har vært viktig, og hvorfor det som var viktig ikke var intuitivt i første omgang.

Tidlig verdi

Målet mitt med Mikrobloggeriet var ikke å ha en kodebase jeg kunne pelle på, det var å skrive sammen om ting vi hadde lært. Jeg definerte verdi som “vi som skriver på Mikrobloggeriet skal få noe ut av å skrive sammen”.

Første milepæl var å få en start på innholdet. Her er committen der Lars Barlindhaug skrev OLORM-1:

https://github.com/iterate/mikrobloggeriet/commit/b42b365f00e1ad7630f21cd43186804aee8e392a

Og her er hele innholdet:

# En god natts søvn

I går holdt jeg på en litt kompleks refaktorering hvor jeg skal hente
fargevarianter for en garnpakke fra to forskjellige steder i databasen, avhengig
av om de er opprettet av designeren av oppskriften eller strikkeren har laget
sin egen fargevariant. Det endte med at jeg ikke ble ferdig før jeg måtte gå
hjem, og jeg så for meg at jeg trengte å bruke et par timer på det i dag. Hadde
jeg blitt på jobb hadde jeg nok også lett brukt et par timer.

Når jeg kom på jobb i dag så jeg umiddelbart hva som var feil, et lite stykke
unna der jeg jobbet i går, og fikset alt på ca 5 minutter.

På det tispunktet fantes ikke nettsiden. Poenget var ikke å lage nettside, poenget var å skrive sammen og dele hva v i har skrevet.

Lekser:

  • Tidlig produktverdi er ofte fullstendig frakoblet hva man har skrevet av kode. Målet med Mikrobloggeriet var å skrive sammen for å dele, lære og utforske, ikke lage webapp. Teknologien for å publisere tekst på nett er velprøvd, vi har hatt HTML i 30 år. Usikkerheten rundt om folk kom til å prøve å skrive én gang, og deretter om folk kom til å fortsette å skrive var betydelig større.

  • Hva som gir mening fra et produktperspektiv og hva som gir mening fra et utviklerperspektiv er her helt forskjellige ting, og begge deler må funke. Hvis du skal ta på deg både produkt og utviklerhatten, må du både definere et verdiforslag du kan ta ut i verden (“product discovery”) og faktisk ta verdiforslaget ut i verden (“product delivery”).

  • Ting som lever i verden og brukes av ekte folk er gjerne enda mer spennende å putle med enn stæsj man koder litt på på egen maskin! Når ekte folk bruker verktøyet du har laget, blir alt mer ekte.

Høy kohesjon

Holdningen min til arkitektur i Mikrobloggeriet var lenge at ett navnerom holder. Jeg vil dra fram ett blindspor, og en suksess. Vi starter med blindsporet.

Helt i starten splittet jeg opp Mikrobloggeriet-koden i to mapper: CLI-et for å skrive innlegg, og HTTP-serveren som skal vise Mikrobloggieriet på Internett. Det lagde flere problemer enn fordeler.

  • Editorer som Emacs og VSCode gir mer hjelp når man redigerer én mappe med kode
  • Når man har én mappe med kode har man ett sett med avhengigheter
  • Når man har én mappe med kode, er å installere avhengighetene til den ene kodebasen noe man gjør én gang
  • Når man har én mappe med kode, er bruken av koden frakoblet fra koden. Man står fritt til å organisere koden der man vil logisk i strukturen, uten å tenke på hvilket underprosjekt som har hvilken funksjonalitet.

Mikrobloggeriet er nå én mappe med én kodebase, og det har fungert mye bedre for meg. Kutt kompleksitet, og bli glad! 😊

I selve server-koden gjorde jeg et valg jeg i dag er mer fornøyd med: putt server-ting i mikrobloggeriet.serve inntill ting har et bedre sted å være. Serveren startet som en router og noen HTTP-handlere. Routeren sier hvor en gitt HTTP-request skal, og HTTP-handlere tar inn en HTTP-request og gir en HTTP-respons. Jeg tror både Johan og Olav har bemerket at mikrobloggeriet.serve er stor! Og det er jeg helt enig i. Store moduler som ikke er “ferdig splittet” er litt vanskelige å sette seg inn i. Det går liksom ikke å “skjønne hele greia” på ett forsøk, man må heller se på hvordan koden brukes i enkelte konkrete tilfeller. Starte med å skjønne hva som skjer på en GET /, gå videre derfra.

Lekser:

  • Ved å splitte opp for tidlig risikerer du å splitte opp på feil måte, som kan gjøre enda mer vondt enn å ha ting samlet en stund. Ekstremvarianten er tidlig, voldsom splitting i separate tjenester, som Olav advarer mot i OJ-4:

    Dette gjør også appen, og eventuelt podden veldig mye mer sårbar til å krasje. Ta hensyn for dette, evnt ved å splitte ut egen logikk for orkestrering, og tenke på retry-mekanismer. Mikroservice-helvette lusker visst overalt. Mitt beste tips her er å sørge for god tilgang til informasjon om ressursbruk i kjøremiljøet.

  • Store moduler kan gjøre vondt når man ikke kjenner koden.

Lav kobling

Så, hva gjør man når modulen har blitt for stor? At modulen er for stor betyr gjerne at den gjør for mange ting. Hvis vi klarer å trekke noe til side, er det bra!

MEN.

Hvis det faktisk skal bli enklere, må det vi trekker til side være lavt koblet med resten av koden. Ellers blir det ikke enklere å jobbe med!

Her kan du vurdere om det funker ved å lese koden direkte.

  1. Leser koden i hovedmodulen bedre etter at du trakk til side et lite problem?
  2. Hvor mange ganger kaller du fra hovedmodulen til sidemodulen?

Hvis hovedmodulen faktisk leser bedre etter at du har splittet opp, og det ikke er alt for mange funksjonskall fra hovedmodulen til sidemodulen, har du lav kobling!

God jul :)

Håper julefreden senker seg hos deg også! Hilsen Teodor

☃❄🎄

Les, skriv, og lær! Det er lov å være nysgjerrig!

Ikke sant, jeg må huske å stille spørsmål til publikum!

Jeg oppfordrer til det sterkeste til å skrive på Mikrobloggeriet, både om ting du lærer (innlegg) og kode. For meg har det vært superviktig, gitt meg et lass av nyttige erfaringer, samt mye mer mot bak meningene mine om produkt og teknologi.

Hvis du synes dette innlegget var spennende, oppfordrer jeg til å skrive et eget innlegg! ITERATE-kohorten er for eksempel et fint sted å skrive hvis du jobber i Iterate.

Lynraske modultester med én tastekombinasjon

Å oppleve lynraske modultester i bruk har endret på hvordan jeg liker å strukturere og jobbe med kode. Kanskje lynraske modultester er noe for deg også?

Hvordan skriver du koden din?

Lynraske modultester med én tastekombinasion er en fryd å jobbe med fordi det gir en super boost til feedbacken du får når du skriver kode. Kanskje noe å vurdere hvis du ikke har prøvd? Les videre for å høre hvorfor du bør ta lynraske modultester seriøst, og hvordan du får det til med Clojure, Javascript, Go og Python.

Hvorfor du bør ta lynraske modultester seriøst

Du kan kode raskere hvis du kan sjekke om koden funker raskere. Hvis du vet nøyaktig hva koden gjør, er du klar til å endre den.

Med lynraske modultester får du bekreftet at koden gjør hva du tror den gjør umiddelbart. Det lar deg endre og legge til funksjonalitet uten frykt. Når du også stoler på at testene er korrekte og dekker det du bryr deg om, kan du også sjøsette ny kode straks den er skrevet og testene er grønne.

Så, hva er god nok feedback fra koden din? Jeg vil beskrive feedback langs tre akser.

  • Forsinkelse. Hvor lenge må du vente? Ti millisekunder? Ti sekunder? Hvis du kan holde feedback-loopen din lynrask er det helt supert! Her bryr jeg meg om “opplevd lynraskt”. En 60 Hz-skjerm gir deg et nytt bilde hvert 16. millisekund. 16 millisekunder er lynraskt! 200 millisekunder er OK. 1 sekund er dårlig.

  • Bredde Hvor mye dekker feedbacken du får? Når testene er grønne, stoler du nok på testene til å gå rett i produksjon med koden? Når du lener deg på modultester i arbeidet du gjør, må du kunne stole på at modultestene dekker det du bryr deg om i modulen.

  • Ergonomi. Å scrolle gjennom tusenvis av linjer printet i et svart vindu med hvit tekst er dårlig for hodet ditt. Se for deg at du kjører en bil, men i stedet for å se veien foran deg, får du en tekstkonsoll med linjer skrevet ut av en kjøreassistent. Det er null sjanse for at jeg setter meg i passasjersetet i den bilen.

Lynraske modultester er bedre målt langs feedback-forsinkelse, feedback-bredde og feedback-ergonomi enn andre mekanismer for feedback fra kode jeg har prøvd før1.

Modultester med én tastaturkombo i Clojure

I Clojure kaller vi modulene våre for navnerom. Hvert navnerom bor i hver sin fil. Vi legger oppførselen til modulen i én fil, og modultestene i et annen fil.

Lag deg en tastaturkombo som gjør følgende:

  1. Lagre filen du har åpen.
  2. Re-evaluer filen du har åpen.
  3. Kjør modultestene, og gi deg selv et sammendrag. (Dette må fungere både når du har markøren din i filen med oppførselen til koden, og når du har markøren din i filen med testene)

Ferdig! Nå har du det!

Hvis du vil prøve Emacs-oppsettet vårt, har magnars/emacsd-reboot denne tastaturkomboen bygget inn som C-c C-k. Andre Emacs-oppsett kan prøve M-x cider-test-run-ns-tests, og for Calva med VSCode kan man sjekke dokumentasjonen for Calva Test Runner. Calva / VSCode har også en helt nydelig støtte for å kjøre flere tastatursnarveier etter hverandre med den fine kommandoen runCommands. Les mer fra Peter Strömberg (skaperen av Calva) i VS Code runCommands for multi-commands keyboard shortcuts.

Jeg anerkjenner at å skrive lynraske modultester kan være en utfordring. Det kan til og med hende at du må tenke på hvordan du kan få testene til å bli raske når du deler systemet ditt inn i moduler.

Modultester med én tastaturkombo i Go og Javascript

Go og Javascript er godt egnet for en kjapp test-loop fordi det er raskt å kjøre en fil. At det er kjapt å kjøre en ny fil kan være et godt alternativ til et kjøretidsmiljø der du kan laste ny kode.

Lag deg en tastaturkombo som gjør følgende:

  1. Lagre filen du har åpen.
  2. Kjør modultestene til filen du har åpen, og gi deg selv et sammendrag.

Når vi ikke bruker et dynamisk kjøretidsmiljø, slipper vi å tenke på hvilken nye kode som skal lastes inn.

Modultester med én tastaturkombo i Python

Python har fått sitt eget avsnitt, fordi etter min erfaring, kan Python-tolkeren ta litt tid når du importerer tunge biblioteker.

Python har imidlertid et dynamisk kjøretidsimiljø! Kanskje du er uenig, eller aldri har hørt om det før? I så fall, sjekk importlib!

>>> import importlib
>>> help(importlib)

Se! Standardbiblioteket kommer med en modul laget for å laste ny kode! Hvis du fremdeles er på Python 2, kan du se etter reload (som ikke krever import av noen moduler).

Så lager du deg en tastaturkombinasjon som gjør følgende:

  1. Lagrer filen du har åpen.
  2. Laster ny kode fra filen med importlib.
  3. Kjører testene for den nylig importerte modulen.

Gjør det!

Koding skal være gøy! Hvis du rigger deg til med solid oppsett for testing, kan du fokusere på hva du vil at koden din skal gjøre, i stedet for å bruke dagen på å pønske på hvorfor koden tryner.

Appendix A: moro med dynamisk lasting av ny kode i Python

I 2017 og 2018 jobbet jeg med styrkeanalyse av en bru som kanskje i framtiden kommer til å krysse Bjørnafjorden. Bjørnafjorden ligger omtrendt midt mellom Bergen og Haugesund. Verktøyet jeg brukte til modellering er Abaqus. Første versjon av Abaqus kom i 1978: da kunne man skrive 3D-modellen sin som tekst i en inputfil, og få resultater. Alt implementert i Fortran!

Dagens Abaqus har både GUI og innebygget Python-miljø. Det muliggjør mer fancy 3D-modellering enn man kunne før. Vi regnet på skipsstøt i Abaqus, og simulerte storm med 3DFloat. For å sørge for at ting stemte mellom modellene, hadde vi Excel-ark og JSON-filer utenfor som beskrev parameterne til modellen. Jeg skrev koden for å bygge opp Abaqus-modellen.

I de første iterasjonene av koden, restartet jeg Abaqus for å kjøre koden min på nytt. Det gikk jeg etter hvert lei av, det var mye venting for å se hva én endring av én linje kode førte til.

Så jeg droppet omstart av Abaqus, og sørget heller for at jeg kunne laste ny kode fra inni Abaqus uten å starte alt på nytt. Da gikk alt drastisk mye raskere enn før.

Jeg fikk mulighet til å open-source en bit av arbeidet med å laste ny Python-kode dynamisk, nå tilgjengelig på github.com/teodorlu/hotload.

Du kan ikke bruke Hotload til å gjøre akkurat hva jeg beskriver her for å få til lynraske modultester, fordi hotload er laget til å lytte på filer, og kjøre kode med effekter på nytt. Det var uansett en spennende opplevelse for meg å se at jeg fikk til å ta kontroll over eget utviklingsmiljø, og at å laste ny kode inn i en kjørende Python-prosess var helt gjennomførbart. Hotload er noe du kanskje kunne skrevet til deg selv, koden er én fil på 300 linjer.


  1. Unntatt tabeller og grafer når man jobber med svære datasett, men da tenker jeg gjerne på det jeg gjør som utforskning og analyse av data mer enn skriving av kode.↩︎

OLORM-56: Dit og tilbake igjen—TDD, TCR fra en REPL og tilbake til TDD

Når er det lurt å skrive tester? Hvordan skriver man tester? Hvorfor skriver man tester?

Effektiv enhetstesting i praksis er lettest å lære fra noen som har jobbet effektivt med enhetstesting før. Jeg prøver meg alikevel på en en tekst. Mest historiefortelling, bittelitt enhetstesting i praksis. Spenn deg fast!

Som utviklere kan vi oppnå en vanvittig effektivitet ved å kontinuerlig vite om systemet vi jobber på er rødt eller grønt, holde oss på grønn, og bli i flytsonen mens vi skriver kode. Tre teknikker du kan bruke for å komme nærmere flyt når du koder er test-dreven utvikling (TDD), test && commit || revert (TCR) og REPL-dreven utvikling (RDD). Hva betyr disse egentlig? Og hva kan du bruke nå?

I dag får dere høre om min reise fra TDD til TCR og RDD, og tilbake igjen til TDD.

Dit og tilbake igjen

TDD for dimensjonering av armering i betong

Den første kodebasen jeg jobbet på etter endt utdanning regnet ut nødvendig mengde armering per løpemeter for betongdekker i Python. Da jeg tok over koden hadde koden null tester. Jeg ble overrasket over at utvikleren turte å implementere denne logikken uten tester. Hva om utvikleren regnet feil? Da kunne jo bygg bli dimensjonert feil?

Det første jeg gjorde i den kodebasen var å innføre tester.

Jeg gikk svært sakte fram, og sjekket hva koden gjorde i dag. Og jeg snakket med en eldre byggingeniør med cirka 40 års erfaring med dimensjonering av betongkonstruksjoner. Sammen bygde vi en forståelse for hva koden skulle gjøre.

Etter at vi hadde innført tester i koden, var det tryggere for meg å endre koden. Testene lot meg sove godt.

Folk legger mange ting i testdreven utvikling, kjent som TDD (fra Test-Driven Development på engelsk). Én av definisjonene er at når du koder, gjør du følgende:

  1. Skriv en ny test som vil bli grønn når du har implementert noe ny kode
  2. Skriv kode som gjør at testen blir grønn på enklest mulig vis
  3. Observer at testene er grønne, eventuelt gjør at testene blir grønne
  4. Rydd i koden så det er tydelig hva koden gjør (kjent som “refactoring” på engelsk).

Man må ikke nødvendigvis skrive test før implementasjon. Men hvis du har tester på koden din, har du bedre kontroll på hva koden gjør. Da er det lettere å rydde i koden, og utvide koden til å gjøre nye ting.

TCR med Elm

Elm er et vakkert, ryddig, lite programmeringsspråk for å lage webapper. Elm-guiden er den beste introduksjonsguiden til et programmeringsspråk som jeg noen sinne har lest. Jeg synes Elm var så bra at jeg lagde og gjennomførte et kurs i Elm-programmering for barn, og snakket om erfaringene på Oslo Elm Day 2019.

Da jeg startet i Iterate fikk jeg jobbe litt med Lars Barlindhaug på Woolit-kodebasen. Vi skrev Elm sammen, og prøvde TCR. Det passet bra, fordi Woolit er skrevet i Elm, Elm er godt egnet for TCR, og Lars var med på bootcampen der TCR ble funnet opp. Lars skriver om sin opplevelse med bootcampen på How to test && commit || revert.

TCR med Elm var en fryd. Typesystemet til Elm er svært kraftig, og når man programmerer Elm sånn Elm er ment til å bli programmert, er det tilnæmet umulig å innføre feil i Elm. En ting som ofte sies om Haskell (et annet programmeringsspråk) er “if it compiles, it runs”. Hvis det kompilerer, funker det. Min erfaring er at det stort sett stemmer for Haskell, og at det ~alltid stemmer for Elm. Elm har et mer konsistent typeystem enn Haskell som er lettere å sette seg inn i ved å ha færre features. Et eksempel er typeklasser, typeklasser er en løsning for polymorfisk dispatch i Haskell. Les wikipedia.org/wiki/Expression_problem for mer info. Philip Wadler nevnes tidlig i Wikipedia-artikkelen, han er en av personene bak Haskell. Elm har ikke typeklasser. Det gjør Elm-kode lettere å lese og lettere å sette seg inn i enn Haskell-kode.

Lars og jeg satte opp TCR til å kjøre “test” som typesjekk. Vi skrev kode, lagret, og gikk kun framover hvis testene var grønne. Det utfordret meg til å tenke i mindre inkrementer.

Litt senere fikk jeg den samme leksa inn med teskje av å jobbe med Oddmund Strømme. Jeg hadde for vane å endre all koden, og være på rød lenge. Det har jeg nå gått tungt bort fra. Nå foretrekker jeg å holde meg på grønn hele tiden, og gjøre refatoreringer som en strøm av kompatible endringer, før jeg til slutt bytter over på ny implementasjon.

Når man gjør dette på teamnivå, kalles det ofte “trunk-based development”.

Umiddelbar feedback for alle kodebaser med REPL

Jeg foretrekker å bruke programmeringsspråket Clojure når jeg kan velge programmeringsspråk. Det er fordi Clojure er et godt egnet programmeringsspråk for REPL-Driven Development. REPL-Driven Development blir også kalt Interaktiv programmering. Hvis du er nysgjerrig på Interaktiv Programmering, er presentasjonen Stop Writing Dead Programs av Jack Rusher en underholdene introduksjon.

Jeg sporer meg selv av. Interaktiv programmering er å programmere fra innsiden av programmet sitt. I stedet for å endre filer som plukkes opp når man rekompilerer eller restarter i en terminal eller med en file watcher, sitter man med en editor koblet til en REPL, der man kan endre oppførselen til egen kode uten å restarte systemet.

Men! Interaktiv Programmering krever trening og disiplin for å brukes effektivt. Du kan lett ende opp i en tilstand der filenes tilstand på disk ikke reflekterer tilstanden til programmet ditt i minne.

Det problemet hadde jeg aldri da jeg skrev Elm med TCR. Jeg visste alltid med 100 % sikkerhet hver gang jeg lagret at koden min passerte typesjekken. Tilsvarende kunne jeg hatt enhetstester, men det hadde jeg ikke, og det følte jeg ikke at jeg trengte. Hvorfor kan jeg ikke få til det samme fra en REPL?

TCR fra inni en REPL

Så, jeg prøvde meg på å løse problemet. Og jeg fikk til det jeg prøvde! Github-repoet teodorlu/clj-tcr beskriver nå hvordan du kan få til TCR i Clojure.

Trikset er:

  1. Lag en ny TCR-snarvei i editor som du bruker i stedet for “evaluér uttrykk” og “lagre fil”
  2. Snarveien gjør følgende:
    • Lagre alle filer
    • Synkroniser tilstand i filer til tilstanden til den kjørende prosessen i minnet
    • Kjør testene
    • Reverter hvis testene feiler, commit ellers.
    • Hvis synkronsiering av tilstand fra filene til den kjørende prosessen feiler, reverterer vi da også.

Her er Clojure-kode som gjør nettopp dette:

(ns user
  (:refer-clojure :exclude [test])
  (:require
   babashka.process
   clj-reload.core
   cognitect.test-runner))

(defn reload [] (clj-reload.core/reload))

(defn test []
  (let [{:keys [fail error]} (cognitect.test-runner/test {})]
    (assert (zero? (+ fail error)))))

(defn commit []
  (babashka.process/shell "git add .")
  (babashka.process/shell "git commit -m working"))

(defn revert []
  (babashka.process/shell "git reset --hard HEAD"))

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn tcr
  "TCR RELOADED: AN IN-PROCESS INTERACTIVE LOOP"
  []
  (try
    (test)
    (commit)
    (println "success")
    (catch Exception _
      (println "failure")
      (revert)
      (reload) ; In those cases where we revert, we choose to clean up our mess
               ; -- don't leave the user with a REPL out of sync with their files.
      )))

Som Oddmund tidligere har sagt om TCR, er dette ikke kode man trenger et bibliotek for å bruke. Tilpass arbeidsflyten til koden og de som skal jobbe med koden.

Så binder du en tastatursnarvei i editoren din til å:

  1. Lagre alle åpne filer
  2. Kjøre user/tcr.

Sånn kan den funksjonen se ut i Emacs Lisp:

(defun teod-clj-tcr ()
  (interactive)
  (auto-revert-mode 1)
  (projectile-save-project-buffers)
  (cider-interactive-eval "(clj-reload.core/reload)")
  (cider-interactive-eval "(user/tcr)"))

Så velger du en tastatursnarvei du vil bruke. For å binde til Option+Enter på en Mac som kjører Doom Emacs, kan du gjøre følgende:

(map! :g "M-RET" #'teod-clj-tcr)

TDD 2: Dommedag

Terminator 2: Dommedag

Hvis du ønsker å bli en bedre utvikler enn du er i dag, bør du jobbe med folk du har noe å lære av. En av utviklerne jeg mener jeg har noe å lære noe av er Christian Johansen. Han er en dyktig programmerer som lager gode biblioteker, og har lang erfaring med test-dreven utvikling og parprogrammering.

På Babaska-meetup i Mai fikk jeg par/mob-programmere med Christian. Jeg kjørte (satt ved tastaturet) og han navigerte (sa hva jeg skulle gjøre). Og han navigerte ved å fortelle meg at jeg skulle skrive tester. Jeg fikk instruksjoner som “skriv en test som sjekker X” og “fiks koden så testen er grønn”.

Jeg innså at vi fikk mesteparten av verdien jeg har fått fra TCR tidligere med god, gammeldags TDD. Skriv koden så den kan testes. Skriv en test som viser oppførselen du ønsker. Kjør testene kjøre, resultatet bør bli rødt. Skriv kode. Kjør testene, resultatet blir helst grønt. Repeat.

Å jobbe med flinke folk er skummelt. De kan ting som du ikke kan. Du føler deg kanskje dømt for at du ikke er flink nok ennå!

Men personene du jobber med er helst ikke en robot sendt tilbake i tid for å ta livet av deg, men heller en trygg utvikler som både ser hva du kan gjøre bedre, og også ønsker å investere i at du kan bli flinkere! Det er ikke til hjelp at noen sier “du, Teodor, du gjør alt feil, og dette er helt håpløst”. I kontrast er det supernyttig når noen viser hvordan de tenker, og stiller spørsmålstegn til rare ting du gjør som du kanskje ikke trenger å gjøre.

TDD fra inni en REPL

Programeringen med Christian fikk meg til å tenke. Jeg ønsker meg følgende:

  1. En REPL så jeg kan evaluere uttrykk
  2. Enhetstester som dekker det jeg bryr meg om
  3. En måte å vite at koden jeg kjører er i synk med koden som kjører på disk
  4. En umiddelbar måte å kjøre enhetstestene.

Så jeg skrev meg kode for å gjøre nettopp det i min Emacs:

(defun teod-reload+test ()
  (interactive)
  (projectile-save-project-buffers)
  (cider-interactive-eval "(do (require 'clj-reload.core) (clj-reload.core/reload))")
  (kaocha-runner-run-all-tests))

(map! :g "M-RET" #'teod-reload+test)

Du kan gjøre omtrent det samme med Visual Studio Code og Calva også:

    {
        "key": "ctrl+[Semicolon]",
        "command": "runCommands",
        "args": {
            "commands": [
                "workbench.action.files.saveFiles",
                {
                    "command": "calva.runCustomREPLCommand",
                    "args": {
                        "snippet": "(do (require 'clj-reload.core) (clj-reload.core/reload))"
                    }
                },
                {
                    "command": "calva.runCustomREPLCommand",
                    "args": {
                        "snippet": "(flush) #_forces-a-print"
                    }
                },
                "calva.runAllTests"
            ]
        }
    }

Ikke test for å teste, test for å gjøre din egen hverdag bedre.

Jeg skriver ikke tester for testene sin skyld. Jeg skriver tester for meg selv og for de andre utviklerne på teamet mitt. Jeg vil ha et godt utviklingsmiljø lokalt, så jeg kan fokusere på å kode, ikke å stirre på stack traces. Og jeg vil ha kontroll på at koden i produksjon gjør det jeg tror den gjør. Derfor skriver jeg tester.

Når jeg møter en kodebase der README sier hvordan jeg kan kjøre testene, og testene dekker det som er viktig i koden min, blir jeg glad!

Appendix A: les Tolkien!

There and Back Again er også tittelen på en bok som er bedre kjent som Hobbiten. Den liker jeg veldig godt!

—Teodor

OLORM-55: Bør du lære deg Vim?

Jeg har skrevet mange ikke-tekniske tekster i det siste. Nå vil jeg skrive en teknisk.

I dag vil jeg adressere om du bør lære deg Vim.

Men jeg vil snakke om Vim ved å ikke snakke om Vim.

Spesialtilfelle Generelt spørsmål
“Bør jeg lære meg Vim?” “Bør jeg lære meg ting som ikke umiddelbart gir meg verdi?”

Det er mange ting du kan lære deg

Kanskje du lurer på slaveri i Libya. Kanskje du vil spille gitar.

Mange kule ting tar tid å lære seg.

Å lære vanskelige ting endrer deg

Når noe går veldig fort å lære kan det bety at læringsopplegget er helt fantastisk og du har hatt en vanvittig læringsfart. Det kan også bety at du egentlig kunne tingen fra før av, og har du har lært en ny måte å uttrykke et konsept du allerede kan.

Det en type vanskelige ting som er vanskelige uten at du egentlig burde trenge å lære deg det. Hvis du sitter og må huske 29 steg for hvordan du prodsetter kode kan det ta lang tid å lære seg, uten at du har lært deg en nyttig, ny ferdighet. Kanskje kunne de 29 stegene vært automatisert bort?

Det finnes en annen type vanskelige ting som er vanskelige fordi de er ting du ikke har gjort før. Du prøver deg, tenker at dette høres interessant ut. Du kaster deg ut i det. Så skjønner du ingen verdens ting. Så føler du ingen mestring.

Da kan du velge én av to ting: du kan avfeie det du prøvde, og sette det som et ikke-mål å lære seg. Eller du kan jekke ned selvbildet. Innse at du har noe å lære. Prøve å nå et enklere mål i samme retning.

Denne typen vanskelige ting endrer deg i læringsprosessen. Du går inn som én person, og går ut som en annen person: en annen person som kan mer. Du føler ikke lenger at det er ukjent, skummelt og vanskelig—det som startet som ubegripelig er nå håndfast. Det har blitt en hammer du kan bruke til ting, uten at du tenker på hvordan du holder hammeren.

Mange vanskelige ting på én gang?

Jeg har noen ganger overvurdert min egen kapasitet til å lære. Jeg setter meg et ambisiøst mål. Så gjør jeg ikke det ferdig, og starter med flere andre ambisiøse mål samtidig! Så blir jeg irritert fordi jeg ikke får framgangen jeg ønsker, og ingen prosessene blir ferdig. De flyter rundt uten at jeg føler at jeg har kontroll på tingene.

Fra dette har jeg lært at simultankapasiteten min for vanskelige ting er én.

Jeg vil ha én vanskelig ting å jobbe med, og noen lette på siden. Da fungerer jeg bra. Hvis jeg har en god dag, jobber jeg på den vanskelige tingen. Hvis jeg har en dårlig dag, jobber jeg på den enkle tingen.

Hva lærer du for tiden?

Hvor står du nå? Hva skulle du ønske at du ble bedre på?

Det er lure spørsmål å stille seg selv! Det er også lure spørsmål å diskutere med en mentor. Når du har tenkt over hva du faktisk ønsker å lære, er det lettere å lære.

Oppfordring: ha én vanskelig ting du jobber med å lære deg

Så, velg deg en vanskelig ting du vil lære! Uttrykk for deg selv hvorfor du vil lære tingen. Si til deg selv hva du vil gjøre når du har har fått kontroll på tingen. Det motiverer!

Så kan du velge å dele målet med andre hvis du vil.

Så, bør du lære deg Vim?

Å lære seg Vim gir null verdi den første måneden. Og null verdi den andre måneden. Men etter hvert snur det, og du er mer effektiv med Vim enn med piltaster og mus.

Jeg estimerer at jeg navigerer og redigerer 10-50 % raskere med Vim-bindings i editoren min enn jeg gjør uten.

Så hvorfor la jeg inn innsatsen? Jeg så en person redigere kode mye raskere enn meg, og tenkte at “hmm, jeg har lyst til å lære det”. Så prøvde jeg av og på ganske lenge før ting satt i fingrene.

Jeg har fremdeles mye å lære om Vim, men nå er de tingene lettere å lære. Jeg er over kneika, og kan lære meg én og én liten ting.

I går lærte jeg meg for første gang find replace, ala :%s/Vim/Programmering i Zig/g. Da satte jeg av en halvtime, leste litt manual, og fikk brukt find replace.

Og ikke lær deg Vim fordi noen har sagt det. Velg én vanskelig ting du ønsker å lære. Kanskje noe en person du har sett allerede behersker, da vet du at det er mulig.

Leke, lære, lage har en gang i tiden vært mottoet til Iterate!

—Teodor

OLORM-53: Forstå brukeren ved å bli brukeren

Å lage produkter for seg selv er forskjellige fra å lage produkter for andre. Hvis du lager produkter for deg selv, kan du vurdere produktet ved å se om produktet løser problemer for deg. Hvis du lager produkter for andre, må du vite om produktet hjelper brukeren i brukerens kontekst.

Er disse tilnærmingene gjensidig eksklusive, med andre ord, må du velge én?

Jeg vil si nei.

Hvordan forstå brukeren ved å bli brukeren

Du kan løse for begge ved å følge to strategier samtidig:

  1. Sett deg mer og mer inn i hva brukeren er som du ikke er. For hver ting brukeren gjør som ikke gir mening for deg, spør deg selv hvorfor, og prøv å finne ut hvorfor.
  2. Lær deg litt og litt av hva brukeren kan.

Da skrenker du gradvis inn forskjellen mellom deg og brukeren. Vurderingene dine om hva produktet bør bli blir bedre og bedre.

Deretter risikerer du å lage noe brukeren ikke klarer å bruke!

Hvis du blir skikkelig god på denne strategien, kan du gjøre jobben til brukeren bedre enn den gjennomsnittelige brukeren. Hvis du da lager produkter for deg selv, blir produktet ikke det brukeren trenger.

Da må du løse for to ting:

  1. Produktet må gi verdi til brukeren der brukeren er nå.
  2. Produktet må fasilitere til kontinuerlig læring, så brukeren kan bli enda bedre på jobben sin ved å bruke produktet ditt kontinuerlig.

Anvendelse ved å skrive kommandolinje-programmer

Jeg har bidratt til kommandolinje-programmet Neil. Neil er et program for å legge til Clojure-pakker når du programmerer med Clojure.

For å bidra fulgte jeg to strategier samtidig:

  1. Gjøre Neil til et bedre verktøy for å løse mine behov
  2. Gjøre en innsats for å forstå hvordan andre bruker Neil på måter jeg ikke ennå forstår.

Den strategien har jeg opplevd at har fungert svært bra, og jeg har begynt å tenke på denne måten når jeg jobber med andre produkter.

Hvis jeg hadde laget et skriveverktøy for journalister, hadde jeg jobbet kontinuerlig med å gjøre skriveverktøyet bedre egnet for sånn jeg ønsker å jobbe, og for å forstå hvordan journalister bruker skriveverktøy på måter jeg ennå ikke gjør. Deretter ville jeg vært litt varsom med å innføre funksjonalitet som kan bli vanskelig å bruke for journalister — enten fordi den forutsetter forkunnskaper journalistene ikke har, eller fordi funksjonaliteten ikke løser for hvordan jeg bør lære å bruke funksjonaliteten.

Ender’s Game av Orson Scott Card er en kul bok

In the moment when I truly understand my enemy, understand him well enough to defeat him, then in that very moment I also love him. I think it’s impossible to really understand somebody, what they want, what they believe, and not love them the way they love themselves. And then, in that very moment when I love them…. I destroy them.

Så … vi vil jo ikke ødelegge brukerene.

Men hvis vi skal overbevise noen om å betale penger for noe vi har laget er utvilsomt en forhandling. Da er det fint å forstå hvor personen du forhandler med kommer fra.

Sitet er hentet fra boka Ender’s game. Les mer om den på Goodreads: https://www.goodreads.com/quotes/97512-in-the-moment-when-i-truly-understand-my-enemy-understand

Den handler om en åtte år gammel gutt som blir sendt ut i verdensrommet for å bli trent til å kjempe mot romvesner. Jeg synes den er fin!

Fortsatt god morgen!

—Teodor

Krøll på tidslinja

Vi har et ganske vanlig oppsett der en applikasjon lagrer tidspunkt i en MariaDB, og jeg oppdaga en dag at noen så ut til å ligge i rar rekkefølge.

Kun på én spesiell dag, om høsten. Mellom klokka 2 og 3 på natta. Du kan kanskje gjette dagen? Det er selvfølgelig når vi skrur klokka en time tilbake, den siste dagen av sommertid.

Koden i applikasjonen ser omtrent slik ut:

var created time.Time
// ...
db.Exec("INSERT INTO t(created,id,info) VALUES(?,?,?)", created, id, info)

Både applikasjonen og databasen kjører med tidssone satt til Europe/Oslo. Tiden som skrives (created) er typisk ganske nært eksekveringstiden, noen sekunder eller minutter før Now().

Det som skjer er følgende:

0. Applikasjon og database kjører med gitt tidssone (Oslo)
1. Applikasjon oppretter en tilkobling til databasen
   som har en tidssone på _sesjonen_ (Oslo)

                 ,------------------------.       +------+
INSERT(ts) ---> (  session time_zone:XXX ( ) ---> |  DB  |
                 '------------------------'       +------+

2. Database-driveren                           3. Verdien konverteres
   i applikasjonen konverterer                    fra sesjonens tidssone
   gitt tidspunkt til sesjonens                   til UTC og lagres
   tidssone og enkoder og sender verdien          (Og oversettes fra UTC
                                                  til sesjonens tidssone
                                                  ved utlesing)

Sesjonens tidssone kan settes av klienten, og arver ellers verdien fra DB-serveren.

Jeg hopper til poenget: Problemet ligger i enkoding av verdien i kombinasjon med sesjonens tidssone. Tidspunktet enkodes som 2023-10-29 02:30:00[.000000] av driveren. Med tidssone Europe/Oslo er denne tiden som kjent tvetydig, fordi den skjer to ganger; først for sommertid, så for vintertid. Enkodingen må derfor inkludere hvilken av dem det er snakk om.

Så, er det bare driveren som er dårlig? Nei, dessverre er det i MariaDB p.t. ikke mulig å enkode tidssone-info verken som binær¹- eller tekst-verdi (i motsetning til MySQL).

Løsningen er å sette sesjonens time_zone og verdiene som skrives til en fast tidssone uten sommertid, f.eks. UTC.

Merk at dette problemet ikke er spesielt for Norge. Selv om stadig færre land har sommertid, er det fortsatt ca 70 land som gjenstår, inkludert nesten hele Europa og Nord-Amerika.

Hva kunne vi gjort for å unngå bug’en? Lite gjennomtenkte muligheter:

  1. Prøve å tenke seg til mulige spesialtilfeller?
  2. Lese hele MariaDB- og driver-dokumentasjonen?
  3. ~Lese all kildekoden?~ (urealistisk)
  4. Teste med alle tidspunkt/permutasjoner?
  5. Kun bruke ukomplisert tech/features?
  6. Detekter (tenkte) problemer når/hvis de oppstår?
  7. QA med eksperter?

Send meg gjerne gode forslag.

—Richard Tingstad

Tillegg: testing av tid

Jeg fant et kult bibliotek jeg kunne bruke for å endre tiden; libfaketime, og lagde meg følgende Dockerfile for å gjenskape tidligere tilstander:

FROM mariadb:11.3

RUN apt-get update && apt-get install libfaketime

ENV TZ=Europe/Oslo \
    MARIADB_DATABASE=d MARIADB_ROOT_PASSWORD=r \
    MARIADB_PASSWORD=p MARIADB_USER=u

COPY <<EOF /docker-entrypoint-initdb.d/0.sql
    CREATE TABLE t ( id INTEGER AUTO_INCREMENT PRIMARY KEY,
        ts TIMESTAMP, dt DATETIME );
EOF

COPY <<"EOF" run.sh
    [ -n "$START" ] || START=$(date -d '2023-10-29 02:30 CEST' +%s)
    LD_PRELOAD=$(find /usr -name libfaketime.so.1) \
        FAKETIME=-$(( $(date +%s) - $START )) \
        "$@"
EOF

ENTRYPOINT ["sh", "run.sh"]
CMD ["docker-entrypoint.sh", "mariadbd"]

(¹ “The encoding of the COM_STMT_EXECUTE parameters are the same as the encoding of the binary resultsets.” —Binary protocol)

Rabbit looking at watch

OLORM-52: On the balance between design and implementation when we build software

Dette innlegget er krysspublisert til Mikrobloggeriet, originalen ligger på Teodors personlige nettside: https://play.teod.eu/software-design-implementation-balance/

X: “design”? You mean the work designers do?

T: No. I mean software design. I have a beef with the current usage of the word “design”. We’re using it vaguely! I used to do design of steel structures and design of concrete structures. These days I often engage in software design.

I feel like the agile movement inadvertently dragged in an assumtion that design is bad, and writing code is better. I think “don’t do design, write code instead” is a really bad idea.

X: So … Why now?

T: I’m working with two great people. Having a lot of fun. But I feel like we struggle a lot finding a balance between design and implementation. Right now, we talk a lot about implementation details, but not about system design.

X: Ah, I get it! You’re talking about architecture! Software architecture!

T: 😅 I … have a bit of a beef with that word too.

X: why?

T: We software developers took the word “architecture” from civil engineering projects, and I fear that we took the wrong word. In software, we want to talk about the core of our system. Architects can do the start of the work, if what you care about is floor plan utilization, and the civil engineering is trivial. Architects do not build bridges. Architects do not build dams. Architects do not build industrial plants. In those cases, the civil engineers (or construction engineers) hold the “core”.

X: so … which word, then, if architecture is bad?

T: I think the word is “design”. And for systems, “system design”!

X: okay. Man, when you insist on redefining words before you even start speaking, it sometimes rubs be the wrong way. It’s like everything I say is wrong, right?

T: Yeah, I know. Sorry about that. I don’t know of a better way — other than leaning completely into the arts, presenting ideas as theater, dialogue or as novels. Steal from Eliyahu Goldratt’s way of presenting things, perhaps.

X: yeah, yeah. I don’t always have time for that, you know?

T: Yeah, there’s so much f-in stuff. I feel like we could make due with less stuff. But that requires some thinking.

X: So … What was that balance you mentioned? A balance between design and implementation?

T: Right. Thanks. That was where we started.

We spend so much time on implementation and so little time on design. And we’re calling it “agile”. “agile” as an excuse for coding up things when we don’t have any idea why we have to do the things we do. If we slow down, we might get pressure to speed up. It’s lean to do less stuff. But rather than cut the problems we don’t care about, we solve the problems we care about badly!

This is where design comes in. We should know what our goal is. I’m … I’m at a point where I have no interest in writing code unless the goal is clear.

X: what about teamwork? Everyone needs to know what do do, right?

T: Yeah, that’s the hard part. It’s harder to solve real problems than write code. And it’s even harder when you’re a team. So much shared context is needed.

X: So, what do we do?

T: I’m discovering this myself as I go along. I’ve had success with two activities.

One activity is pair programming. This one is hard. Knowing when to focus on design and when to focus on implementation is hard. I think great pairing is something you have to re-learn every time you pair with someone new. Without trust, this will simply fail. And that trust needs to go both ways. I need to trust you, and you need to trust me.

Another activity is to use a decision matrix to compare approaches to solve a problem. A decision matrix lets you do clean software design work without getting stuck in all the details.

X: How should I learn pair programming?

T: Ideally, you get to pair with someone who is good at pair programming. I had the chance to pair with Lars Barlindhaug in 2019 and Oddmund Strømme in 2020. From Lars, I learned that it’s better to organize your code into modules where each module solves a problem you care about. From Oddmund, I learned that I could work in smaller increments.

If you do not have someone you can learn pairing from on your team, watch Magnar Sveen and Christian Johansen pair from their youtube screencasts: https://www.parens-of-the-dead.com/

X: And … those decision matrices?

T: Watch Rich Hickey explain decision matrices in Design in Practice. Then try it out with your team!


Thank you to Christian Johansen for giving feedback on an early version of this text.

—Teodor

Linjer i fil A som ikke er i B

Hvis du har to tekstfiler, A og B, hvordan finner du alle linjene i A som ikke er i B?

A - B

(Et eksempel-brukstilfelle er å filtrere ut linjer som allerede er behandlet.)

Mitt goto-verktøy er grep 👇

grep

grep -F -f B -v < A

# search a file for a pattern
# 
#   -F, --fixed-strings
#     Match  using fixed strings. Treat each pattern specified as a string
#     instead of a regular expression.  (-F is specified by POSIX.)
# 
#   -f  pattern_file, --file=pattern_file
#     Read one or more patterns from the file named by the pathname
#     pattern_file. Patterns in pattern_file shall be terminated by a <newline>.
#     (-f is specified by POSIX.)
# 
#   -v, --invert-match
#     Select lines not matching any of the specified patterns.
#     (-v is specified by POSIX.)

grep er generelt veldig rask, men med stor pattern_file så går det sakte. Hvis filene kun består av ASCII kan vi bruke prefiks LC_ALL=C grep for å øke ytelsen noe, men det hjelper ikke så mye. Hvilke andre alternativ har vi?

hash set

Ett nivå lavere enn å kjøre en database er å bare huske linjene i minnet vha. f.eks. Python, Node.js eller awk:

awk 'BEGIN{
  while(getline < "B") seen[$0]++
  close("B")
}
!seen[$0]' < A

Awk består av ett eller flere pattern{ action } der BEGIN er et spesielt pattern som kjører før input (A) behandles, og !seen[$0] får default action { print $0 } siden ingen er angitt. ($0 er ei tekstlinje.)

Hvis du ikke vil holde hele B i minnet eller bruke et ekstra programmeringsspråk finnes det noen flere enkle kommandoer. 👇

join

Hvis A og B har én identifiserende kolonne (eller ett ord), kan vi bruke sort og join:

sort -oA A
sort -oB B

join -v 1 A B

# join - relational database operator
#
#  The join utility performs an “equality join” on the specified files and
#  writes the result to the standard output.  The “join field” is the field in
#  each file by which the files are compared.  The first field in each line is
#  used by default.  There is one line in the output for each pair of lines in
#  file1 and file2 which have identical join fields.  Each output line consists
#  of the join field, the remaining fields from file1 and then the remaining
#  fields from file2.
#
#    -v  file_number
#      Instead of the default output, produce a line only for each unpairable
#      line in file_number, where file_number is 1 or 2.

Hvis du ikke vil tenke på kolonner, kun hele linjer, er siste kommando perfekt. 👇

comm

comm -2 -3 A B

# compare two sorted files line by line
#
#  three text columns as output: lines only in file1, lines only in file2, and
#  lines in both files.
#
#    -1      Suppress the output column of lines unique to file1.
#    -2      Suppress the output column of lines unique to file2.
#    -3      Suppress the output column of lines duplicated in file1 and file2.

Denne visste jeg ikke om før nylig, men den er like grunnleggende som grep — fra 1973! (De nyere join og awk er “bare” fra 1979.)

Jeg synes disse gamle verktøyene er kjempefine og nyttige den dag i dag.

Har du andre måter å filtrere ut eksisterende linjer? Jeg vil gjerne høre.

— Richard Tingstad

OLORM-50: For mye opsjonalitet

Hvor mye bør man eksperimentere i bredden? Når bør man smalne sammen?

I design thinking snakkes det om expandering (expand) og kontrahering (contract).

  • Under ekspandering dyrker vi forskjeller. Vi trenger først å finne muligheter før vi kan begynne å gjøre ting.
  • Under kontrahering utforsker vi én retning. Vi tar en løst formulert idé og gjør den ekte.

Så hva er opsjonalitet? En opsjon er noe du kan velge å gjøre. Mye opsjonalitet er bredt handlingsrom. Du har mange ting du kan velge å gjøre. Lite opsjonalitet er smalt handlingsrom. Du har én eller ingen ting du kan gjøre.

Symptomer på for lite opsjonalitet

Når det gjøres én bred beslutning tidlig og den følges slavisk, hvordan vet man at man gjør rett ting? I produktsammenheng snakkes det om “outcome over output”. Målene du setter bør være effekter du vil oppnå, ikke ting du skal gjøre. Hvorfor? Fordi målet er ikke at det gjøres jobb. Målet er å løse problemer!

Når jobben blir målet, er typisk problemet for lite opsjonalitet. I stedet for å løse problemer, gjør vi oppgaver.

Symptomer på for mye opsjonalitet

Så, hva skjer når du får for mye opsjonalitet?

  • Du tenker på mange forskjellige problemer som ikke har så mye med hverandre å gjøre.
  • Ting faller i glemmeboka.
  • Du får ikke fullført ting du begynner på.
  • Du føler at du mister oversikt.

Begrens work in progress!

Når du har for mye opsjonalitet, må du begrense work in progress. Du må begrense hvor mange ting du driver med samtidig. Det gjør du ved å si nei!

Men du trenger ikke si “nei, det kommer jeg ikke til å gjøre, det du spør om er dumt”. Fordi det er ikke det du mener. Det du mener er å si at du ikke har kapasitet til å begynne å se på dette nå.

Kutt scope

For mye opsjonalitet for jobben du gjør betyr at du gjør for mange ting på én gang.

Det kan også være for mye opsjonalitet i produktet du lager.

I Mikrobloggeriet har vi støtte for temaer. Du kan scrolle nederst på forsiden og velge tema. Det synes jeg er gøy! Vi har laget oss handlingsrom til å eksperimentere med temaer.

I dag lager denne eksperimenteringen friksjon. Olav og Neno har jobbet med stilsetting av nettsiden. I det arbeidet har ikke temasystemet vært til nytte—det har vært til hinder.

Hva kan vi gjøre? Én mulighet er å slette temasystemet og begynne på nytt.

I kodespråk er temasystemet en dårlig abstraksjon. Temasystemet skulle abstrahere spesifikke temaer vekk fra generelle regler. I praksis er abstraksjonen for lite fleksibel, fordi Neno og Olav ikke får gjør det de ønsker.

Har du passe mye opsjonalitet i hverdagen?

Hvis du har for lite opsjonalitet kan du utfordre oppgaver og spørre hvorfor dette skal gjøres. Hvis du har for mye opsjonalitet, kan du si nei til jobb eller kutte variasjon i produktet du lager.

Vil du ha mer lesestoff?

Les Let a 1,000 flowers bloom. Then rip 999 of them out by the roots. av Peter Seibel. Den er bra!

—Teodor, 2024-02-09

Send signaler til kommando

Jeg kjører for tiden en kommando som tar lang tid og inneholder en sleep for å ikke overvelde systemer den kaller:

cat big | while read in
  do
    echo $in
    sleep 1
  done | process

Ulempen med statiske, langlevde kommandoer er at endringer krever omstart.

Jeg ville justere sleep 1 under kjøring, og en måte å gjøre dette på er å bruke signaler:

cat big | (
  trap 'i=$((i+1))' USR1
  trap 'i=$((i-1))' USR2
  sh -c 'echo Signal $PPID' >&2
  i=1
  while read in
  do
    echo $in
    sleep $i
  done) | process

(Visste du at du kan blande imperativ kode inn i en pipeline sånn?)

Hvis denne skriver ut Signal 139 kan jeg gjøre:

kill -s USR1 139  # inkrementer $i
kill -s USR2 139  # dekrementer $i

—Richard Tingstad

P.S. Inspirert av (GNU) xargs.

OLORM-48: Løs kobling

Jeg har trampet høylytt og sagt at du er ansvarlig for å bygge delt forståelse av hva som er bra, innenfor din ekspertise. Jeg er også ansvarlig for å bygge delt forståelse av hva jeg synes er bra.

Så hva er god kode?

God kode er løst koblet

God kode er delt i moduler der hver modul er løst koblet fra andre moduler.

Men hva er løs kobling?

Jepp, det er det gode spørsmålet. Jeg vil ikke komme med en formell definisjon av hva løs kobling er; jeg vil heller komme med noen eksempler.

tettere koblet løsere koblet
package.json har 20 avhengigheter package.json har 16 avhengigheter
modul A bruker 6 funksjoner i modul B modul A bruker 3 funksjoner i modul B
modul A bruker modul B, modul B henter tilstand i applikasjonen selv module A bruker modul B, all informasjon sendes som eksplisitte funksjonsparametre
for å jobbe på en app, kan jeg kjøre kun koden jeg bryr meg om for å jobbe på en app må jeg starte 7 ting jeg ikke vet hva er
enhetstestene krever at databasen kjører enhetstestene krever ikke at databasen kjører
jeg må ut og teste i test- eller prodmiljøer for å finne ut om det funker jeg kan kjøre det jeg bryr meg om lokalt

Liksom-løs kobling som egentlig er tett kobling

Å splitte kode i noe kode en plass og noe kode en annen plass gir oss ikke nødvendigvis løsere kobling. I verste fall har vi like tett koblet kode, som nå er to plasser. Da har vi alle problemene vi hadde før (koden er fortsatt tett koblet), men enda et problem: vi må huske på at noen av koden er et annet sted, og vi må vite hva som skjer der for å få gjort noe.

Det er vanskelig å sikre løs kobling

Da jeg først hørte James Reeves si noe ala “developers develop a smell for coupling and learn to avoid it”, skjønte jeg ikke hva han mente. På det tidspunktet hadde folk betalt meg for å skrive kode i nærmere fem år. Hva betyr det, liksom? Hva har det å si?

I dag ser jeg på kobling som en tung ryggsett. Hver kobling koden din har til annen kode er en stein i ryggsekken. Det er tyngre å gå når ryggsekken er full av stein!

Ekstrem kobling: sykliske avhengigheter

Hvis modul A kaller på modul B, og modul B også kaller tilbake på modul A, er ting så ille som de kan bli! Du kan nå verken endre A uten å endre B, eller endre B uten å endre A. Her er to ting du kan gjøre:

  1. Slå sammen til én modul. Hvis begge modulene bruker den andre, har du ikke fått fordelene med å splitte i moduler. Så gå videre med én! Kanskje dette burde være én modul. Skal du bruke gjensidig rekursjon (“mutual recursion”), begge funksjonene kjenne hverandre (med mindre du bruker en tilstandsmaskin eller noe sånt, men det har ikke jeg hatt behov for å gjøre i praksis ennå.)

  2. Splitt koden i fire. Trekk først ut grensesnittet mellom modul A og modul B. Putt grensesnittet i modul G. Skriv nå modul A og modul B om til å kun være avhengig av grensesnittet. I statiske språk blir dette typer, og/eller “interfaces” (Go, java), “traits” (Rust) eller typeklasser (Haskell). Skriv en siste hovedmodul H der du bruker både modul A og modul B. Til slutt får du disse avhengighetene:

    H -> A
    H -> B
    A -> G
    B -> G

    Hovedmodulen (H) heter ofte “main”, og vet det den må vite for å starte applikasjonen.

For å få løsere kobling, må du sannsynligvis gå saktere fram.

Når du har noe som funker, tar du deg tiden til å se på hvordan koden henger sammen? Burde koden henge sammen på den måten? Den koden du skriver blir liggende i kodebasen! Når du nettopp har skrevet koden, kjenner du best hvordan den fungerer. Senere blir det vanskeligere å splitte opp.

—Teodor

Java 21 pattern matching

Pattern matching i Java har blitt bra!

JEP 441: Pattern Matching for switch

For eksempel, la oss lage en ny Optional-type av records og sealed interface:

sealed interface Maybe<T> {}
record Some<T>(T val) implements Maybe<T> {}
record None() implements Maybe {}

(En record er bare en enkel data-klasse som ikke kan utvides eller muteres. På grunn av sealed vet kompilatoren nå at Maybe kun kan bestå av Some og None.)

Det stilige nå er at vi kan gjøre switch som sikrer håndtering av alle mulige tilfeller:

String fun(Maybe<Integer> n) {
    return switch (n) {
        case None ignore -> "nothing";
        case Some(var i) -> String.format("%d.0", i);
    };  // ^begge trengs for å få kompilert!
}

Det er mulig å nøste og kombinere, la oss lage en til type:

public sealed interface Contact {
    record Email(String mail) implements Contact{}
    record Phone(int prefix, int number) implements Contact{}
}

Og vi kan matche slik:

void handle(Maybe<Contact> c) {
    switch (c) {
        case None none -> {}
        case Some(Contact.Email e) -> sendEmail(e.mail);
        case Some(Contact.Phone p) -> phone(p.number);
    }
}

Om vi ønsker kan vi matche Phone(int pre, int num) i stedet for Phone p også :)

(P.S. Hvis de ulike typene ikke ligger samme sted som sealed-interface-et/klassen trengs permits: sealed interface Contact permits Email, Phone {)

—Richard Tingstad

OLORM-46: kortsiktig og langsiktig arbeid med kode

I prosjektet jeg jobber på nå, skal jeg inn, gjøre en jobb, og ut igjen innenfor planlagt tid. Det er mye kode, og jeg har ikke tid til å lære meg alt. I tillegg har kunde hatt høye krav til hva som må inn, og begrenset budsjett.

Jeg føler på kroppen at jeg ikke er i stand til å gjøre de beste beslutningene om hvordan en ting bør løses i kodebasen. Jeg har tid til å sette meg delvis inn i eksisterende arkitektur og hvordan det har blitt jobbet, men for å faktisk komme i mål med det jeg skal gjøre må jeg sette strek. Det er det pragmatiske valget; kunden har et behov, jeg kommer og gjør en jobb, og skal gjøre den jobben til budsjettert tid. Kodeteknisk vet jeg at det er ting jeg bør ta tak i som jeg ganske enkelt ikke rekker.

Jeg er overbevist om at kode kan være objektivt god.

  • Fiks en flaky test? Objektivt god forbedring.
  • Sørg for at testene kjører i CI? Objektivt god forbedring.
  • Redusér tiden det tar å kjøre testene fra 10 sekunder til 0.2 sekunder? Objektivt god forbedring.

Denne typen forbedringer får jeg sjelden tid hvis jeg er “inn og ut” på et prosjekt.

Å bli en god utvikler krever langsiktighet nok til at man rekker å sette seg inn i ting, bli produktiv til å jobbe med koden, så oppleve effekten av arkitekturen på egen effektivitet, så oppleve effekten av forbedret arkitektur.

Akkurat sånn har jeg lært å programmere. Jeg har satt opp et prosjekt. Prosjektet har etter hvert løst problemet det skal løse. Så har jeg merket at enkelte biter av arkitekturen lugger. Så har jeg endret arkitekturen, og fikset problemet.

Bør man lære seg å jobbe med produkter og lære seg å jobbe med kode samtidig? Jeg mener nei. Uten god kode er det null sjanse for at vi klarer å lage gode produkter. Men med svært solid kompetanse på kode, er det mye lettere å lage gode produkter. Kan du gjøre en solid forbedring på koden på én dag? Bra. Er du ikke der ennå? Da kan ting forbedres! Du kan forbedres, koden din kan forbedres.

Og når forbedringer på produktet kan shippes på én dag i stedet for på to uker, kan jeg love deg at jobben med å lage et godt produkt blir lettere.

Kjersti skriver om å avfeie kundens meninger i LUKE-6 - Kunden tar alltid feil!:

Kanskje vi ville bli bedre på å ta utgangspunkt i kundens hypoteser dersom vi samlet sett vet at vi ikke blir «tatt» på å feile litt lenger ned i løypa (ref punkt 1). Her har jeg egentlig et veldig stort spørsmål: hvordan balanserer vi vårt eget behov for å forstå med kundens forståelse av markedet vi skal inn i? Jeg har en følelse av at vi blander kortene her.

Hvis vi ikke tar kundens hypoteser seriøst, bør ikke kunden leie oss inn.

Grav i hva hypotesen er. Vær ekstremt nøye på de spørsmålene. Men ikke påstå at det er feil! Ikke før du har prøvd.

Tidpunktet å diskutere hypotesene er etter en hypotesetest, ikke før. Snakk om hva den første effekten vil være hvis det vi gjør går bra. Det er hva du vil teste. Snakk om hvilken tidshorisont kunden ser for seg å få til det på. Flott, da vet du cirka hvor stor denne klumpen med arbeid er.

Så kan du se nøye om det faktisk fungerte ikke når du har prøvd på ekte.

Er dette mye å forvente? Ja! Bli gjerne god på å kode først. Men ikke avfei det kunden sier før dere har prøvd: følg Principle of charity før du setter deg selv i kritikkmodus. Bonus: når du viser villighet til å prøve, begynner kunden å stole på deg. Når kunden stoler på deg, er det mer sannsynlig at du blir hørt.

—Teodor

OLORM-45: --scale i Docker Compose

I dag lærte jeg noe kult!

Docker Compose har et --scale-argument man kan bruke til å skru av en tjeneste. Supernyttig når jeg vil kjøre én tjeneste manuelt (feks med go run eller yarn dev), men vil ha resten i Docker.

Eksempel:

docker compose up --scale api=0 --scale analytics=0

Så kjører vi api og analytics manuelt i hver sin terminal:

(cd api && make run)
(cd analytics && make run)

Tidligere har jeg kommentert ut og inn tjenester av docker-compose.yml. Det har jeg aldri helt likt. Det er alltid en sjanse for å comitte sånne endringer og ødelegge for andre.

Kudos til Christian Duvholt som skrev Docker Compose-filen som gjorde alt dette mulig.

—Teodor, 2023-11-24

OLORM-44: HTMX

Jeg er for tiden urimelig opptatt av HTMX. Hvorfor?

  1. Hypertekst er en enkel mental modell som lener seg på at nettlesere tilbyr interaktivitet dersom du lager hyperdokumenter.
  2. Med HTMX kan du skrive ren statisk HTML så lenge det holder, og innføre litt og litt dynamikk der det trengs.

Dette gjør at du kan levere interaktive nettsider i alle programmeringsspråk som egner seg til å skrive en HTTP-server.

Du slipper i tillegg å håndtere tilstand på klienten: med HTMX har du kun tilstand på serveren.

Da visker vi bort skillet mellom server og klient. Du lager en webapplikasjon, og kan bruke én teknologi for å lage alt.

Denne fordelen får du også hvis du allerede bruker Javascript, språket som kan kjøre direkte i nettleseren: i stedet for å skrive en server og en klient, skriver du kun én webapplikasjon. Én applikasjon er lettere å vedlikeholde enn to.

Jeg synes ofte det er rart å snakke med folk om hypertekst. Jeg synes hypertekst er en ufattelig god teknologi—vi bygger opp internett som et sett med dokumenter, der dokumenter kan lenke til andre dokumenter. Informasjon står først, systemer og kode er en detalj. Dokumentene er det vi leverer. Dokumentene er det brukerne våre tar i.

Men å bruke HTMX krever at man tenker nytt! Som ofte er tilfellet når man jobber med teknologi. Det er mange nye ting man kan lære seg, hvilken bør man velge? For 2024, vurdér HTMX. Ordet “hypertekst” ble brukt av Ted Nelson i 1967. Jeg har tro på en comeback.

Færre, enklere verktøy for en enklere utviklerhverdag.

Lær mer om htmx på htmx.org.

—Teodor

OLORM-43: Fire akser for å vurdere kvalitet—presisjon, generalitet, innovativitet og levendehet

Avatar får terningkast 6! Det er en dritkul film!

Men kul film for hvem, til hva? Filmkritikere fyller på med tekst som beskriver hva de likte, hva de ikke likte, og hva slags stemning man fikk av å se filmen.

Hvordan sammenlikner du Mona Lisa med The Emperor’s Old Clothes, turing-forelesningen til Tony Hoare? Gir du begge 6/6 og sier deg ferdig? Eller setter du deg ned og skriver tekst for å dele din egen opplevelse av Mona Lisa og The Emperor’s Old Clothes?

Når filosofer snakker om hvordan vi beskrver ting, bruker de ofte fenomenologi eller intersubjektivitet for å unngå fella om “alt er subjektivt, derfor kan vi ikke si noe som helst”. Men fenomenologiske og intersubjektive betraktninger er ofte lange, og vanskelige å lese.

Jeg foreslår i stedet at vi velger oss fire akser med verdier mellom 0 og 1.

Presisjon, generalitet, innovativitet og levendehet: fire akser for å klassifisere tekst

  • Presisjon. Er det tydelig hva som formuleres? Eller er det åpent for tolkning? En god tekstbok i fysikk er presis. Et godt skjønnliterært verk er nødvendigvis mindre presist.
  • Generalitet. Snakker vi om generelle sannheter om mennesker? Eller uttaler vi oss om et smalere domene? Er dette interessant for noen eller mange? Evolusjonsteori lar deg uttale deg om en bred samling domener. Hvordan vi lager mikrochipper som bruker layout i 3D til å gi lokalitet for å lage raskere CPU-er er smalere.
  • Innovativitet. Er det som presenteres allmenkjent, eller kommer vi faktisk med ny kunnskap her? En tekstbok om matematikk til bruk på videregående skoler skal ikke presentere et innovativt pensum: vi en solid gjennomgang av kjent materie. En person som forsker på tallteori kunne uttale seg om noe nytt. Ellers er det ikke forskning!
  • Levendehet. Blomstrer teksten, eller er det sten død? Vi vil at standup-komedie sjokkerer oss. Vi vil ha kunst som beveger oss. Vi vil føle noe når vi er i naturen. Men vi vil ikke bli overrasket over at taket vårt faller ned. At pengene våre i banken blir borte. At skip synker. Noen tekster lever, får oss til å føle.

The War of Art vurdert etter presisjon, generalitet, innovativitet og levendehet

Jeg har gitt boka The War of Art av Steven Pressfield 5/5 stjerner for Goodreads, sammen med kommentaren:

Tacky title. First time I tried reading it I hated it after one chapter and put it away.

It’s opinionated and unapologetic. It might offend your feelings.

But it’s honest. Good luck.

I dag vil jeg vurdere den etter presisjon, generalitet, innovativitet og levendehet fordi jeg tror det gir mer dybde enn stjerner.

Presisjon: 0.7. Pressfield skriver sabla godt, men språket hans blomster av allegorier. Dette er litteratur, ikke forskning. I blant er det uklart ha han mener. Men den åpningen gir det mulighet til å reflektere.

Generalitet: 0.5. Boka handler om å skape. Den skriver på en måte som fint er mulig å kjenne seg igjen i. Men den polariserer. Jeg tror ikke boka er for alle.

Innovativitet: 0.9. Ideene i boka var nye for meg da jeg leste boka (på anbefaling fra Sean Percival). Du skal skape, ellers kan du gi opp at det skjer nye, uvendede ting.

Levendehet: 0.99. The War of Art er en fryd å lese, og en reise i god historiefortelling. Jeg blir mer gira av å ha lest et kapittel. Det hender jeg plukker opp kapitler og leser kapitlene på nytt bare for å få med meg stemingen kapittelet setter meg i. 0.99 er den høyeste scoren på levendehet jeg har gitt noen tekst noen sinne.

Husk å tagge vurderinger med dato.

Jeg skrev vurderte The War of Art på 2023-10-15 (søndag 15. oktober 2023). Kanskje jeg endrer på hva jeg mener! At du endrer på hvordan du vurderer en tekst sier noe om hvordan du endrer deg. Spennende greier! For å ta høyde for dette i et databaseskjema, er det fint å legge ved en datotagg. Når er denne vurderingen gjort? Og av hvem?

Vi gjør subjektive vurderinger på et punkt i tid. Så kan vi heller mene andre ting senere hvis vi har endret oss siden sist.

Hvordan vurderer du tekst?

Når du har sett en film, lest en bok eller vært på slam-poesi, hvordan framstår det for deg at du har vært på noe bra? Hva er det du setter pris på? Dette er jeg meganysgjerrig på. Jeg tror aksene mine speiler personligeheten min ganske tydelig. Hvordan passer de for deg? Ville du lagt til andre akser? Svar i tråd!

sed matching og betinget utførelse

“Match X og print Y=f(X)” er noe man ofte ønsker å gjøre. Eksempel:

echo 'Her er noe eksempeldata med
en linje med referanse til ref_123_bg og
en til ref_436_md osv.' > fil

Si at vi vil:

  1. Finn alle “ref_<ID>_<SUFFIX>
  2. Print <SUFFIX>:<ID>

En enlinjes sed-kommando for dette er:

#            Søkeuttrykk              Erstatning
#          ┌─┴─────────────────────┐ ┌┴──┐
sed -Ee 's/.*ref_([0-9]+)_([a-z]+).*/\2:\1/;t' -e d <fil
bg:123
md:436

Det jeg liker med denne er at vi bare trenger ett regulært uttrykk i skriptet.

Kommandoen t er det magiske her, det er seds eneste kommando for betinget flytkontroll.

UNIX PROGRAMMER’S MANUAL 1979

Fra dagens manual kan vi tilføye:

If label is not specified, branch to the end of the script.

Dette er grunnen til de to -e option-argumentene; vi trenger et “linjeskift” for å avslutte t uten label-argument.

Skrevet ut ser skriptet slik ut:

sed -E '
    s/.*ref_([0-9]+)_([a-z]+).*/\2:\1/    # erstatt *ref_X_Y* med Y:X
    t    # hvis erstatting skjedde, hopp til <label> (tom = slutten)
    d    # slett linje (hvis ikke hoppet over av forrige funksjon)
'        # de linjene som ikke slettes vil som default printes

Da jeg lærte denne komboen av -e og t (og b, betingelsesløs forgrening) syntes jeg den var akkurat så sær at jeg måtte legge den i mitt hjerte, og dele den med dere i dag.

Hva synes du?

—Richard Tingstad

P.S. Sett sammen med hold space omtalt tidligere så kan man kanskje skimte her at programmeringsspråket til sed er Turing-komplett.

Kotlin?

Nå driver jeg og lærer meg Kotlin i et nytt prosjekt.

Jeg er usikker på om jeg liker det.

Kotlin i seg selv virker ok. Men jeg sitter for øyeblikket med Spring Boot, som jeg definitivt ikke liker.

Jeg har lurt på om Kotlin er aller kulest om du stort sett har jobbet i Java tidligere.

Jeg har savnet IDE-støtte for automatiske kodeendringer, håper det blir mer av det på meg når jeg får brukt Kotlin mer.

OLORM-40: trust, but verify

Betyr det å stole på folk å slippe kontrollen?

Det er lite givende å hjelpe til når en person tviholder på all kontroll, og vil ta alle beslutningene. Når en person skal bestemme alt, er det lite plass til mitt bidrag.

Én mulighet er å slippe kontrollen fullstendig. Før gjorde jeg jobben, nå er det noen andre som gjør jobben. Det får bli hva det blir.

Ulempen med å slippe kontrollen fullstendig er at man ikke får en overgangsperiode. Kunnskap blir borte.

Et alternativ er å gi tillit, men verifisere hva som skjedde.

Gi rom til at folk kan gjøre sitt beste. Bidra etter beste evne selv. Så ser du hva som skjer. Og etterpå verifiserer du om det som skjedde var det du ønsket at skulle skje. Er du fornøyd eller misfornøyd? I begge tilfeller bør du kanskje snakke med personen som gjorde jobben.

—Teodor


Frasen “trust, but verify” ble mye brukt av Reagan under avtalene for nedrustning av atomvåpen på 80-tallet. Frasen har sin egen side både på Wikipedia og på Wiktionary. Jeg har også skrevet om grad av kontroll i lederskap i kontekst av et Elm-kurs jeg lagde og holdt for Kodeklubben Oslo i 2017.

GitHub knakk byggene våre

I går feilet plutselig flere av våre CI-bygg uten tydelig årsak.

Etter tur innom noen blindgater der vår kode var endret — men tilsynelatende ikke relevant — fant jeg til slutt årsaken etter kjøring av flere modifiserte test-bygg for feilsøking.

Vi bruker i mange GitHub Workflows:

    runs-on: ubuntu-latest

(Eller ubuntu-22.04 eller ubuntu-20.04, det samme gjelder.)

Disse runner-image’ene fikk nylig en oppgradering: Ubuntu 22.04 (20230903) Image Update, 20.04 (20230903) Image Update, der de endret blant annet:

Category Tool name Previous (20230821.1.0) Current (20230903.1.0)
Tools Compose v2 2.20.3 2.21.0

Docker Compose release notes 2.21.0 sier at:

The format of docker compose ps and docker compose ps --format=json changed to better align with docker ps output.

Sistnevnte kommando returnerte tidligere en json-array, men returnerer nå linjeseparerte json-objekter. Dette er en breaking change (ikke-bakoverkompatibel endring).

Jeg synes dette er en litt kjedelig situasjon. At Docker Compose brekker APIet sitt så lemfeldig. At vi har ikke reproduserbare bygg fordi bakken endrer seg under føttene våre. At dette er normalsituasjonen?

Hva tenker du?

—Richard Tingstad

Start på slutten

I utvikling følger vi ofte en Kanban-inspirert prosess med tavle inndelt i kolonner med oppgave-lapper:

innkommende pågående verifisering ferdig
ny sak
fiks
oppgave

Kanban sier: Begrens antall oppgaver under arbeid!

Vi vet jo alle at for mange samtidige oppgaver skaper krevende kontekstbytter, generell fare for kaos, og økt total tidsbruk per oppgave — så dette er et fornuftig råd.

En vakker innsikt er å alltid lese tavlen fra høyre til venste (systemet er pull-basert, ikke push-basert). Hvilken oppgave er nærmest å være ferdig? En ferdig oppgave leverer verdi og innsikt, og frigjør plass til neste oppgave.

Jeg finner kvalitet i at team-medlemmer følger denne prioriteringen med “sist først”. Det betyr at jeg synes det er verdifult at en pull-request tas relativt raskt, på bekostning av å fortsette med sin egen koding e.l.

Tenk gjerne: “Hva kan jeg gjøre denne morgenen for å unblocke en oppgave?”

Når en ny oppgave skal påbegynnes (vi har en del individuell frihet ved valg av uløst oppgave) finner jeg kvalitet i at man velger “kjipe” bugfiks-oppgaver fremfor kul ny feature. Selv om disse er likestilt “horisontalt” på tavla, tenker jeg at bugfiksen er mer “høyrestilt” (= høyere stilt?) enn en ny feature.

Men så må man selvfølgelig passe på at man også bruker nok tid på de mer langsiktige oppgavene.

Relatert mikroblogg av undertegnede: Fart i utvikling?

Hva tenker du, har du noen andre tommelfingerregler du liker? Er du uenig i mine tanker? Det er lov, jeg er det ofte selv!

—Richard Tingstad

OLORM-37: Expected/actual i hypotesetesting

Jeg har flere ganger prøvd å få hjelp til bruk av Open Source-teknologi, og fått følgende svar i fleisen: “Not clear what expected/actual is. What are you trying to achieve? What are you observing?”

Først skjønte jeg ikke hvorfor jeg fikk dette spørsmålet. Er ikke svaret åpenbart? Koden min krasjer. Jeg vil at den ikke skal krasje.

Men. 🥁 🥁 🥁 Det finnes uendelig antall måter koden kan ikke krasje på! Hvilken er det du egentlig vil ha?

Dette gjelder også hypotesetesting! Hvis du går ut til folk som kanskje skal bruke produktet du lager og spør “er dette bra?”, vet du ikke hva du får svar på.

Jeg liker å formulere en hypotese først. Hypotesen prioriterer. Den sier noe om hva som er viktigst å finne ut. Så kan jeg teste den.

sed reverse

En mye brukt kommando for å reversere linjerekkefølgen i en fil er følgende:

printf 'R\nE\nV\n' | sed -n '1!G;h;$p'
V
E
R

Siden sed-kommandoene er veldig konsise kan de fremstå litt kryptiske. Her gir jeg en forklaring.

sed virker sånn at for hver input-linje evalueres kommandoene.

Ved kjøring kalles linjen under behandling for “pattern space”. Det finnes en tilleggsverdi kalt “hold space” (i utgangspunktet tom), som man kan lagre til om man vil.

Option -n betyr at sed ikke skal printe noe output med mindre vi kaller p. Uten -n printes pattern space etter hver behandlede linje.

1!G;h;$p kan utvides til:

1!{  # linjenr != 1
  G  # hent og append hold space til pattern space
}
h    # lagre (pattern space) til hold space (overskriv)
${   # siste linje
  p  # print (pattern space)
}

Etter hver kommando ser verdiene slik ut:

linje kommando pattern space hold space kommentar
R 1!G R kjører ikke
R h R R
R $p R R kjører ikke
E 1!G E\nR R
E h E\nR E\nR
E $p E\nR E\nR kjører ikke
V 1!G V\nE\nR E\nR
V h V\nE\nR V\nE\nR
V $p V\nE\nR V\nE\nR print

Så hver linje blir i praksis prepend’et de forrige, litt som en stack.

Håper sed nå er mindre magisk!

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

Enums i postgres, trenger jeg det egentlig?

Jeg har eksperimentert med å bruke enums i postgres for kolonner som kun kan ha få gitt versjoner, som for eksempel hvilken state eller type noe er. Det er fint for å sørge for at databasen aldri inneholder noen verdier vi ikke forventer i backenden, men å endre på en enum har vist seg å være litt vanskelig. Å endre på enums i postgres ble introdusert i versjon 9.1, men siden jeg gjør databasemigrasjoner med sqlx som kjører i en transaction, får jeg ikke lov til å endre på enums direkte. Jeg må lage en ny og gjøre en rekke operasjoner for å få endre på noe

ALTER TYPE subscripton_type_enum RENAME TO subscripton_type_enum_old;

CREATE TYPE subscripton_type_enum AS ENUM ('custom', 'free', 'basic', 'designer', 'professional');

ALTER TABLE design_studio_checkout_sessions
  ALTER COLUMN subscription_type DROP DEFAULT,

  ALTER COLUMN subscription_type TYPE subscripton_type_enum
  USING subscription_type::text::subscripton_type_enum,

  ALTER COLUMN subscription_type SET DEFAULT 'custom';

ALTER TABLE design_studio_subscriptions
  ALTER COLUMN subscription_type DROP DEFAULT,

  ALTER COLUMN subscription_type TYPE subscripton_type_enum
  USING subscription_type::text::subscripton_type_enum,

  ALTER COLUMN subscription_type SET DEFAULT 'custom';


DROP TYPE subscripton_type_enum_old;

Siden backend-koden er skrevet i Rust og det naturlig nok kun er den som snakker med databasen, tenker jeg at det kanskje er like greit å holde seg til å kun definere enums i backenden og ikke i databasen i tillegg.

Trenger vi dokumentasjon?

Jeg kom akkurat ut fra nok et møte om en løsning som har integrasjon mot to tredjeparter.

Begge har vi eksisterende integrasjoner mot, men nå skal vi buke noe ny data, noen nye parametre, og ny versjon av noen endepunkt.

Jeg synes det er utfordrende at det er lite og mangelfull dokumentasjon.

Hva betyr egentlig feltet nId i respons-dataen? Er feltet externalX alltid satt, eller kun for noen typer?

Noe av dette er dokumentert på side 13 av et arbeidsdokument fra i fjor. Annet er “dokumentert” i et svar i en av hundrevis av Slack-tråder.

“Alle” misliker å skrive og oppdatere dokumentasjon, og alle hater utdatert og misvisende dokumentasjon.

“Koden er dokumentasjonen,” sies det. Den er aldri utdatert(?). Men for tredjeparts APIer har vi ikke koden, vi har bare stikkprøver av data og gjetninger.

Så jeg vi si: ja, vi trenger dokumentasjon.

Jeg tror vi hadde spart mange møter, duplikate Slack- og epost-spørsmål hvis vi hadde det. Hvis ikke tredjeparten leverer dokumentasjon, kanskje vi skulle dokumentert APIet på vår side, som best vi kan.

—Richard Tingstad

OLORM-33: Et første møte med CICD

Alle kan Github actions, ikke sant? Og YAML, det må da folk ha hørt om.

Nope! Vi utviklere må lære nye ting hver dag. Det er en fantastisk mulighet; hvis vi kontinuerlig lærer nye ting, kan vi bli skikkelig flinke. Det er også en byrde: det finnes så ufattelig mange ting vi må lære oss.

Jeg har nettopp satt opp CICD på Mikrobloggeriet. Her er mine erfaringer.

Hva er CICD?

Først, la oss definere hva vi snakker om.

  • Continuous integration (CI) handler om at vi kontinuerlig sjekker om systemet vårt fungerer når vi endrer det. I Mikrobloggeriet betyr CI “Vi kjører alle enhetstestene på alle commits”.
  • Continuous delivery (CD) handler om at vi kontinuerlig oppdaterer det kjørende systemet. Vi kan sette koden vår i produksjon når vi vil. I Mikrobloggeriet betyr CD “Vi setter automatisk alle endringer i produksjon hvis testene er grønne”.

Om å kode på mikrobloggeriet

Når jeg har jobbet med Mikrobloggeriet-koden har jeg fulgt noen prinsipper:

  1. Vi skal se produktverdi før vi legger mye jobb i koden. Hvis ingen vil skrive mikroblogger, skal vi heller ikke lage et stort system for mikroblogging.
  2. I koden skal vi vente med å abstrahere. Vi vil heller ha for spesifikk enn for abstrakt kode.

Jeg synes det har fungert bra til nå. Men det har gitt noen utfordringer:

  1. Vi lagde mikroblogg med 41 innlegg før vi skrev en eneste enhetstest.
  2. Da den første andre personen enn meg skulle prøve å spinne opp koden, ble det trøbbel.

Da er det på tide å sakke ned! Jeg ville da få til følgende:

  1. Kodebasen legger opp til at man kan skrive tester og jobbe mot testene lokalt.
  2. Testene kjører mot hver commit på Github (CI)
  3. Vi prodsetter automatisk kode når testene er grønne (CICD)
  4. Kodebasen inneholder ingen uferdig eller ubrukt kode (“clean code”)

Noen refleksjoner om CICD

Hva synes jeg om CICD etter å ha prøvd litt?

Utrolig fint å kunne lene seg på grønne tester i commit-loggen. Det gir meg ro!

Testene kjører dobbelt! Jeg kjører både testene gjennom Github Actions og i selve bygget (i Docker). Det føltes litt rart å velge det, er ikke dette duplisering? Jeg vil kjøre testene i en GH Action fordi da får jeg god tilbakemelding på hva, spesifikt som feiler. Og jeg kjører testene mine i Docker i bygget for å unngå at koden blir prodsatt hvis testene feiler. Jeg kunne kanskje sagt at “prodsetting skal vente på at alle sjekker er ferdig”. Men jeg synes det jeg har funker helt fint, og nå vet jeg i tillegg at alle ganger jeg bygger med Docker lokalt er testene grønne.

Github Actions og YAML er noe man må lære seg! Her er en start:

  • For å starte med GH actions, legg til én fil: .github/workflows/test.yml

  • Her er et minimalt grønt eksempel:

    name: Run tests
    on: [push, pull_request]
    jobs:
      Testing:
        runs-on: ubuntu-latest
        steps:
          - run: echo success!

    Hvis du legger til denne i repoet ditt, bør du se en liten grønn prikk ved siden av commit-ene dine.

  • Her er et minmalt rødt eksempel:

    name: Run tests
    on: [push, pull_request]
    jobs:
      Testing:
        runs-on: ubuntu-latest
        steps:
          - run: FAIL

    Eksempelet feiler fordi FAIL ikke er en systemkommando. I stedet for run: FAIL kunne man kjørt run: go test, run: npm test eller run: clojure -A:run-tests. Da skal go test gi returkode 0 hvis alt er OK, og noe annet enn 0 hvis testene feiler.

Er testing og CICD verdt innsatsen?

Min konklusjon så langt er ja. Hvis man ikke har god testdekning og kontroll på hvilke commits som er grønne og røde, blir det utrygt å skrive kode. Og når det er utrygt å endre kode, er det vanskeligere å komme framover på produktet.

Hva er dine erfaringer med CICD?

CICD er ikke noe vi lærer om på universitetet. Det var i alle fall ikke noe jeg var innom. CICD handler om hvordan vi jobber sammen i praksis.

  • Har du vært på prosjekter hvor CI eller CD har latt deg jobbe mer effektivt? Hvorfor?
  • Har du innført CI eller CD på et prosjekt noen gang? Hvordan har det vært?

Jeg vil gjerne høre! Dette er noe jeg tror det er lurt at vi snakker om og deler erfaringer om.

—Teodor

OLORM-32: Konsulentsalg av teknologer

Jeg skrev akkurat masse skryt om meg selv i et konsulenttilbud. På ett vis føles det helt feil.

Her maler vi med så bred pensel at all tekstur er borte. Her er en liste med teknologier! Her er masse greier jeg har gjort tidligere! Jeg lover at alle tingene jeg har gjort tidligere er skikkelig bra!

Så er det bra? Er det dårlig? Tja. Det er i alle fall nødvendig. Vi driver konsulentbedrift, og det er sånn konsulentsalg funker. Hvis de som kjøper folk de ikke er fornøyd med, kan de i alle fall si opp kontrakten. Men kanskje man faktisk blir fornøyd, selv om man får noe man ikke liker.

Go interface nil values

Vi brente oss på en Go-finurlighet igjen.

Vi returnerte interfacet io.Reader fra noen funksjoner, og vi hadde en sjekk if reader == nil.

Dette virker noen ganger fint, og andre ganger ikke. Se følgende kode:

func main() {

    var reader io.Reader = nil
    fmt.Println(reader == nil) // true

    var b *bytes.Reader = nil
    fmt.Println(b == nil)      // true

    reader = b

    fmt.Println(reader == nil) // false 🤯
}

bytes.Reader implementerer io.Reader. Derfor kopilerer dette uten problemer, og kjører også helt perfekt med ulike verdier, bortsett fra akkurat den vist over.

Det viser seg at implementasjoner av interface er nil kun hvis verdien og typen er nil.

Go FAQ har et innslag om dette og anbefaler bruk av interfaces i returtyper, selv om:

This situation can be confusing

De har ellers ingen gode løsninger utover:

Just keep in mind that if any concrete value has been stored in the interface, the interface will not be nil.

Jeg synes dette var veldig overraskende og føler at Liskov Substitution Principle (LSP) blir brutt, men får bare prøve å ha dette i mente.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

Anker-dato

Jeg lærte et nytt ord som jeg synes var interessant nok til å dele. En anker-dato (anchor date) brukes når man f. eks skal betale noe hver måned, men samtidig ta høyde for at ikke alle måneder er like lange.

Her er et eksempel: Et månedsabonnement begynner 31. januar, dette vil også være anker-datoen. Neste gang forfaller det 28. februar. Men når i mars skal det forfalle? Plusser vi på en måned fra siste dato ender vi opp med 28. mars, men det blir ikke riktig. Vi skal heller ta utgangspunkt i anker-datoen og plusse på to måneder. Riktig forfall blir da 31. mars.

Om argument-parsing i Shell

Det finnes noen utbredte konvensjoner som er veldig fint å støtte, som at det er lov å gruppere og sortere argumenter etter eget ønske (denne bloggposten anbefaler jeg):

program -i -t -a
program -it -a
program -tai

Det er derfor nyttig å bruke hjelpe-kommando eller -bibliotek for å lese argumenter. Fish har en ganske fin argparse, som ligner på noe som finnes i mange programmeringsspråk. Om Shell ble det sagt:

argparse. Synes det er skikkelig dritt i sh.

Hvordan kan man gjøre det i shell? De ulike shellene - Bash, Zsh, etc. - har sine egne fine løsninger. Man kan se på disse spsialtilfellene om man vil.

Nesten alle kommandolinje-konvensjonene ble etablert i tidlige Unix-versjoner, på 70- eller 80-tallet. I 1986 kom Bourne Shell med en nyere kommando for argumentlesing: getopts, og:

In 1995, getopts was included in the Single UNIX Specification [POSIX] […] As a result, getopts is now available in shells including the Bourne shell, KornShell, Almquist shell, Bash and Zsh. — Wikipedia

Den er litt annerledes enn argparse, men virker godt:

all=0
while getopts f:aV opt
do
    case $opt in
        V) version; exit;;
        a) all=1;;
        f) file="$OPTARG";;
        ?) printf "Usage: $0 [-a] [-f file] args\n   or: $0 -V\n";;
    esac
done
shift $((OPTIND - 1))

Konklusjon

Med getopts i en while-løkke kan man enkelt deklarere og lese options med (f:) og uten (a,V) option-argument. Hvis man klarer seg uten --long-options er det bare å nyte ☀️ og 🎵. Ellers må vi jobbe videre ⛏ 🕳 🐇…

Tillegg

En utfordring med Bourne Shell er at det ikke har mange datastrukturer. Man kan ikke returnere en hash-tabell eller et objekt, man må loope over tekstfelter.

Jeg lekte meg med å bygge støtte for --long-option og kom opp med:

# usage:
# args=$(parselong a/all f/file -- "$@")
# eval "set -- $args"
# while getopts f:a...
parselong() {
    c=1
    for arg; do
        [ "$arg" = "--" ] && break
        c=$((c + 1))
    done
    [ $c -eq 1 ] && return

    for arg; do
        case "$arg" in
            ?/*)
                short="${arg%/*}"
                long="${arg#*/}"
                end=
                i=0
                while [ $((i+=1)) -le $# ]; do
                    [ "$1" = "--$long" ] && [ -z "$end" ] \
                        && o="-$short" \
                        || o="$1"
                    [ "$1" = "--" ] && [ $i -gt $c ] && end=1
                    set -- "$@" "$o"
                    shift
                done ;;
            --) break;;
            *) echo "unexpected: $arg"; exit 1;;
        esac
    done
    shift $c
    for i; do
        quoted=$(printf %s\\n "$i" \
            | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/")
        shift
        set -- "$@" $quoted
    done
    echo "$@"
}

Nå virker --file foo --all også! Det var ikke trivielt, men jeg synes det var et lærerikt og morsomt dypdykk i shell-argumenter 🙂

Send gjerne spørsmål eller kommentarer til Richard Tingstad

openr

Jeg har laget et lite skript som jeg bruker ofte.

Hva?

Det er et skript som åpner en tilfeldig fil i en teksteditor.

Du kan eksplisitt velge teksteditor med --editor/-e, ellers velger skriptet hva enn du har i $EDITOR.

Om du bruker vi, vim, nvim, lvim får du også åpnet fila på en tilfeldig linje.

Om du bruker hx, code, codium får du også åpnet fila på en tilfeldig kolonne på en tilfeldig linje.

Hvordan?

function openr
  argparse e/editor= -- $argv
  or return

  if set -ql _flag_editor
    set editor $_flag_editor
  else
    set editor $EDITOR
  end

  set file       (git ls-files | shuf -n1)
  set line_text  (cat -n $file | shuf -n1)
  set line       (string sub -l6 $line_text | string trim)
  set position   (shuf -n1 -i1-(string sub -s6 $line_text | gwc -c))

  switch $editor
    case hx
      $editor $file:$line:$position
    case vi vim nvim lvim
      $editor $file +$line
    case code codium
      $editor --goto $file:$line:$position
    case '*'
      $editor $file
  end
end

Usage

Man kjører programmet slik:

$ openr

eventuelt feks slik:

$ openr -e emacs

eller

$ openr --editor /Applications/Xcode.app/Contents/MacOS/Xcode

fish

fish, som dette er skrevet i, er en modernere sh/bash/zsh. Det er mye bedre å skrive i, og er ikke POSIX-kompatibelt. Det vil si at du ikke kan klistre inn vilkårlige shellscripts fra internett og forvente at fish kan kjøre det. Men det er uansett ikke så lurt.

argparse

  argparse e/editor= -- $argv
  or return

Her bruker vi fish' finfine argparse-kommando til å definere og parse kommandoargumenter. Vi sier at vi har étt argument, editor (kortform: e) som har et parameter (=), og vi skal lese dette inn fra $argv (som er en liste over alle ordene man skriver etter openr).

Denne editor-verdien blir så stappet i de implisitte variablene $_flag_editor og $_flag_e.

  if set -ql _flag_editor
    set editor $_flag_editor
  else
    set editor $EDITOR
  end

Her sjekker vi om $_flag_editor er oppgitt, og setter isåfall $editor til den verdien.

Om $_flag_editor ikke er satt setter vi $editor til å være hva enn $EDITOR er.

Finne frem filer og posisjoner

set file       (git ls-files | shuf -n1)

Her lister vi ut alle filene git vet om i mappen (og undermapper) man står i med git ls-files. Så pipes dette inn i shuf -n1 som gir oss én tilfeldig linje av det som blir pipet inn, altså et tilfeldig filnavn. Vi setter dette i variablen $file.

set line_text  (cat -n $file | shuf -n1)

Her lister vi ut innholdet i fila vi fant i forrige steg. Med -n får vi også med linjenumre (som vi bruker i et senere steg.) Så pipes dette inn i en shuf -n1 som over. Så vi står igjen med $line_text som inneholder en tilfeldig linje fra fila i $fila, med linjenummer foran.

set line       (string sub -l6 $line_text | string trim)

Her skraper vi ut de seks første tegnene i $line_text som vi definerte over, og luker vekk whitespace med string trim. Vi sitter igjen med linjenummeret fra den tilfeldige linja i variablen $line.

set position   (shuf -n1 -i1-(string sub -s6 $line_text | gwc -c))

Denne bråkete linja fjerner linjenummeret fra $line_text og teller antall tegn med gwc -c. Vi bruker gwc over wc fordi wc på MacOS putter inn whitespace før tallet vårt. Så tar vi og generer en liste av tall fra 1 til det antallet tegn vi nettopp fant. Så velger vi oss ut et tilfeldig av disse tallene. Og setter det i $position.

Som du kanskje ser er narrativet i koden og prosaen nokså forskjellig her. Beklager om det er vanskelig å følge, men vi sitter i hvert fall igjen med et tilfeldig kolonnenummer.

switch

  switch $editor
    case hx
      $editor $file:$line:$position
    case vi vim nvim lvim
      $editor $file +$line
    case code codium
      $editor --goto $file:$line:$position
    case '*'
      $editor $file
  end

Her velger vi hvordan vi vil starte editoren basert på innholdet i $editor.

  • Er editoren vår hx starter vi $editor (hx) med en syntaks som spesfiserer linje og kolonne
  • Er editoren vår en vi-variant, får vi bare linje
  • Er editoren vår en VSCode får vi både linje og kolonne
  • Er editoren ukjent får vi bare fila

Hvorfor?

Jeg synes det er nyttig å av og til åpne en tilfeldig fil i et prosjekt og lese den og reflektere litt over hva den gjør/er og hvorfor den eksisterer. Er det en gammel bit med kode? Hvordan passer den inn med den nye koden vi skriver? Ser jeg noen åpenbare forbedringer jeg kan gjøre her og nå? Kanskje denne koden kan hjelpe meg å se resten av koden på en litt annen måte?

Makro-kappløp

Race conditions er kanskje mest omtalt i forbindelse med tråder, men jeg fant en artig en.

På prosjektet har vi mange tjenester, og en veldig nyttig kommunikasjonsform mellom dem, er meldingskøer. Spesifikt bruker vi Kafka-køer. En vanlig egenskap ved meldingskøer er FIFO-oppførsel: First In, First Out. Altså at rekkefølgen til meldingene blir bevart.

Kafka skiller seg fra mange andre meldingskøer ved at køene gjerne deles opp i partisjoner for å få høyere gjennomstrømming.

En topic med 4 partisjoner

Hver melding har et innhold, og en key. Denne key’en brukes til å velge partisjon, og innenfor hver partisjon er man garantert FIFO-rekkefølge.

Vi gjorde noe slikt:

Her blir ID (A, B) brukt som key. Hvis A og B beholder innbyrdes rekkefølge gjennom hele løypa går alt fint.

Men følgende blir et problem:

  1. B, endret Adresse, fyker gjennom boksene til Behandling
    1. Leser Ordre A, Status bestilt
  2. A, endret Ordre, kjører også gjennom løypa og ankommer Behandling
    1. Ordre A, Status kansellert, sendes videre
  3. Ordre A fra punkt 1 blir sendt videre

Vi ender opp med en Status: bestillt etter “kanselleringen”.

Konklusjon: Ikke anta at meldinger ankommer i gitt rekkefølge gjennom et distribuert system (med ulik key).

Løsninger på det aktuelle problemet kan være:

  1. Behandling kan sende “Ordre A er endret” i stedet for “Ordre A har fått status X”
  2. Bestilling kan alternativt ignorere utdaterte meldinger ved å sammenligne Versjon e.l.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

OLORM-25: Markdown-lenker på tre forskjellige måter

Markdown støtter å lage lenker. Men du kan lage lenker på forskjellige måter! Her er tre alternativer.

Metode 1: Inline-lenker:

Markdown:

[teodor](https://teod.eu)

HTML:

teodor

Metode 2: Referanse-lenker med valgt navn:

[teodor][1]

[1]: https://teod.eu

HTML:

teodor

Metode 3: Referanse-lenker med implisitt navn.

Markdown:

[teodor]

[teodor]: https://teod.eu

HTML:

teodor

Av de tre, foretrekker jeg referanse-lenker med implisitt navn.

  1. Det er minst kode
  2. Det innfører færrest antall nye navn i koden. Med andre ord, det minimerer mengden abstraksjon.
  3. Det utfordrer meg til å gi lenken min et godt navn.
  4. Det blir minimalt med visuell støy rundt lenken. Gigantiske lenker midt inni et avsnitt, som [til en fil](https://github.com/iterate/olorm/tree/65be6088ae2b54b4b7e9413acaed8327d220ec84/serve/src/olorm/devui.clj) gjør at avsnittet blir vanskelig å lese når man leser plaintext.

Såvidt jeg vet støttes alle tre i Github Flavored Markdown og i CommonMark. Jeg foretrekker å bruke Pandoc til å jobbe med Markdown, og Pandoc støtter CommonMark. Her i Mikrobloggeriet bruker vi Pandoc til å konvertere Markdown til HTML.

—Teodor

OLORM-26: Unix på godt og vondt

Da jeg skrev første versjon av olorm-kommandolinjeprogrammet, ble jeg superengasjert da jeg fant ut at jeg kunne bruke miljøvariabelen EDITOR. git bruker EDITOR til å la brukeren styre hvilket program brukeren ønsker å redigere Git-commit-meldinger med. Hvis du vil skrive commit-melding med Vim, kan du kjøre EDITOR=vim git commit. EDITOR=emacsclient -nt gir deg en Emacs-instans i terminalen, og EDITOR=code -w gir deg Visual Studio Code.

Adrian skrev til og med en shell-funksjon så han fikk det som han ville, og kunne skrive j i stedet for EDITOR="open -a blablabal" jals create. Nå finner jeg ikke koden, men jeg mener å huske at han gjorde noe sånt:

# i ~/.zshrc
j() {
    EDITOR="open -a markdownedit -w" olorm create
}

Så Adrian kunne få til det han ville helt på egen hånd! Det synes jeg er dritkult, vi får i tillegg løftet opp hva Unix/Posix egentlig er.

På den andre siden:

  1. Vi har støtt på uforutsette problemer /hver gang/ nye personer har installert CLI-et.
  2. CLI-et har tilgang til mappesystemet, som gjør at vi åpner for at folk som skriver OLORM og JALS kan bli utsatt hvis jeg (Teodor) blir utsatt for angrep via avhengigheter.
  3. Mange folk bruker ikke terminalen. Det hadde vært dritgøy å få designere, produktledere og andre til å skrive.

Men! Jeg mener fremdeles vi har tatt gode valg.

  1. Vi har shippet
  2. Og vi har gjort det på en måte som har fått de som har skrevet til å fortsette å skrive.

“Funker ikke perfekt for alle helt ennå” kan vi løse i fremtiden, hvis vi velger å fokusere på det.

Refactoring is dead

La oss slutte å snakke om refactoring.

Her er en uutfyllende liste over hva man kan mene når man sier refactoring:

  • Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior. [Refactoring.com]

  • Å utføre en av de navngitte refactoringsene i Refactoring-boka [MartinFowler]
  • Å gjøre en “trygg” automatisk kodeendring/code action i feks IntelliJ
  • Å gjøre en vilkårlig automatisk kodeendring i feks IntelliJ
  • Å gjøre en manuell “trygg” kodeendring
  • Å endre kode uten å brekke tester
  • Å rydde i kode
  • Fikse linterfeil
  • Skrive flere tester
  • Optimalisere kode til å kjøre raskere
  • Koding som ikke er dekket av en JIRA task
  • Koding (hva som helst)

Så selv om du kanskje vet hva du mener når du sier refactoring er sjansen stor for at den som hører/leser ikke vet hva du mener. Kanskje de erstatter din oppfattelse med sin egen. Eller kanskje de, kloke av skade, erstatter med sin egen og legger til et buffer av vaghet og godvilje.

Når du sier refactoring er det sansynlig at mottaker sitter igjen med mindre informasjon enn du mente å gi.

Det er kjedelig å miste informasjon. Men jeg synes det er et større tap at hver gang du sier refactoring kunne du sagt noe annet som er mer eksplisitt og mer beskrivende. Hvor interessant er det egentlig om endringen din er en refactoring eller ikke? Kan du ikke heller si hva du gjorde?

  • Erstattet en klump string literals med en Enum-type
  • Dro parse-logikken ut i et eget modul
  • Sorterte funksjonene så det er lettere å lese dem fra toppen og ned
  • Inverterte noen nesta ifs så vi kan gjøre en earligere early return

Generell find & replace

Search & replace av tekst i filer er kjekt med sed -i (in-place):

find . -name \*.txt -exec sed -i.bak 's/foo/bar/g' {} \;

Men det er ofte kommandoer ikke har et in-place-flagg. Da kan man kjøre (eksemplifisert med jq .):

find . -name \*.json -exec sh -c 'jq . {} >tmp && mv tmp {}' \;

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

En fin dag med Elm

I går hadde jeg en veldig fin dag med Elm programmering, og det er ganske lenge siden sist jeg satt meg ned og lagde noe nytt i Elm. Alle nye frontender vi lager har blitt React/TypeScript, først og fremst for å enklere komme tettere på browseren i noen tilfeller og bibliotekstøtte i andre, sekundert er også bekjentskapen til Elm på teamet en faktor.

En Elm modul består av et view, en modell og en update funksjon for å gjøre endringer på modellen. I tillegg har man en init funksjon for å sette opp modellen, ofte med data fra backend. Vi har en rekke forskjellige måter å bestille strikkeplagg på, det kan være uten annen input enn ditt eget hode, et produkt vi har laget i Sanity, en digital strikkeoppskrift eller nå snart, ditt eget design i en 3D-modell.

For hver av de forskjellige måtene å bestille på henter vi data fra forskjellige steder. I browseren får forskjellige url-er. Modellen og viewet er helt likt i alle tilfeller, men hver url leder til forskjellige init-funksjoner som henter data fra riktig kilde. Det blir veldig oversiktlig og enkelt. For hver init funkson trenger vi også ett innslag i update-funksjonen som setter dataen inn i modellen.

Filtrering av grupper av innslag

På prosjektet har vi en relasjonsdatabase med en ganske stor tabell med over 30 millioner rader. Innslagene inneholder tidspunkt med “item”s status med mer. Ca slik:

+----------+---------+------------+---------+----
| id       | item_id | version_id | state   | ...
+----------+---------+------------+---------+----
| 45649802 |   11111 |          1 | A       |
| 45649811 |   11111 |          2 | B       |
| 45649817 |   12222 |          2 | C       |

Vi oppdaget at noen status-innslag manglet, og trengte å hente ut alle disses item_id. Vi ville finne alle som har state B, men ikke har hatt state A. Jeg liker HAVING SUM(CASE for slike spørringer:

SELECT item_id
FROM history
GROUP BY item_id
HAVING SUM(CASE WHEN state = 'A' THEN 1 ELSE 0 END) = 0
   AND SUM(CASE WHEN state = 'B' THEN 1 ELSE 0 END) > 0

Men når rekkefølge teller, liker jeg LEFT JOIN WHERE NULL:

SELECT t1.item_id
FROM history t1
LEFT JOIN history t2
    ON t2.item_id = t1.item_id
    AND t2.version_id > t1.version_id
LEFT JOIN history t0
    ON t0.item_id = t1.item_id
    AND t0.version_id < t1.version_id
    AND t0.state = 'A'
WHERE t1.state = 'B'
    AND t0.item_id IS NULL
    AND t2.item_id IS NULL ;

Denne sier to ting:

  1. At B er siste status (t2 med nyere rad finnes ikke; IS NULL).
  2. At ingen tidligere rad med status A finnes (t0).

Hvis dette feiler (SQL’en tar for lang tid), kan man velge å frigjøre seg fra databasen:

mysqldump -u USER -p \
    --single-transaction --quick --lock-tables=false \
    DATABASE history > hist.sql

grep INSERT hist.sql | tr \( \\n | grep , > hist.csv     

sort -s -t , -k 2 -k 3 hist.csv > sorted.csv

De første kommandoene her tar noen minutter. sort bruker lenger tid på store filer, men er veldig robust.

< sorted.csv awk -F "'?,'?" '{
    if ($2 != prev) {
        if (match(states, "B$") && !match(states, ",A,"))
            print prev
        states = ""
    }
    states = states "," $4
    prev = $2 }' > itemIDs.txt                                              

For hver item_id ($2 = kolonne 2): print hvis siste state var B og ingen tidligere state A fantes.

Konkusjon

Det er sikkert mange mulige måter å løse denne oppgaven på, dette var én måte.

For meg virket LEFT JOIN-teknikken bra, og jeg synes de deklarative SQL-spørringene er enklere å lese og forstå enn den mer imperative AWK-koden.

Jeg liker dog at man har muligheten til å løse (og dobbeltsjekke) oppgaven på flere måter.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

Frontend og backend

Det vi lager deler vi ofte opp i en frontend og backend og mye er åpenbart om skal plasseres i frontend eller backend, men noen ting er ikke like rett frem.

Med moderne frontendrammeverk blir det lettere og lettere å skrive mer og mer av logikken til applikasjonen i frontenden, men en del ting vil være mer effektivt å plassere så nærme databasen som mulig. Og det blir kanskje spesielt tydelig etterhvert som applikasjonen vokser.

En ting jeg ser flere ganger når jeg utvikler endepunkter i backenden vår er at frontend-utvikler i litt for stor grad godtar at det de får av data fra backenden ikke er optimalt. Om man må gjøre først en request og så 5 til, bør man heller ta en runde til med utforming av API-et så man får akkurat det man trenger i frontenden.

Når jeg utvikler både frontend og backend selv blir det ofte at jeg begynner med backenden, så prøver å løse frontendproblemet og så går noen runder frem og tilbake til jeg finner den optimale løsningen. Men så er også en av mine svake sider at jeg ikke er så flink til å planlegge godt på forhånd.

Å isolere feil når du ikke har tid til å fikse dem

Om du eller noen andre finner en feil oppførsel i et dataprogram du jobber med er det vanlig at første steg videre er å reprodusere feilen.

Er feilrapporten god nok og du har god nok innsikt i koden din, kan det være at du kan reprodusere feilen direkte i en eksempeltest. Det er et ypperlig utgangspunkt for å fikse feilen.

Men av og til er jobben det vil ta å fikse feilen så stor at du ikke har tid til å gjøre det. Ellers kan det hende at feilen ikke er grov nok til at du prioriterer å fikse den over andre ting du vil gjøre.

Du vil gjerne ikke la feilende tester ligge å slenge i kodebasen din, så da sletter du den og lager kanskje en GitHub issue eller skriver om feilen på en post-it.

Men om du er kul og lur, beholder du testen din. I stedet for at den tester at ting går bra, får du den til å teste at feilen produserer en god og unik feilmelding.

I testen kan du kanskje skrive en kommentar om at du ikke prioriterer å fikse feilen her og nå.

Om du bruker Sentry eller noe lignende, kan du nå spore og følge med på feilen. Du kan referere til den og snakke og tenke om den.

Jeg synes dette er bedre enn å ignorere feilen til du får tid/lyst til å fikse den.

Go chans & Unix pipes

Jeg har lagt merke til likheten mellom Go channels og Unix (named) pipes.

Go’s approach to concurrency […] can also be seen as a type-safe generalization of Unix pipes.

Effective Go - Share by communicating

Først, hva er named pipes igjen?

command | grep foo

er en “vanlig” pipe og kan ses på som en spesialisering av en named pipe:

mkfifo mypipe
command > mypipe &
<mypipe grep foo
rm mypipe

Selv om det opprettes et filnavn, vil ikke data skrives til fila. Named pipes gir mye mer fleksibilitet for sending og lesing av meldinger enn en navnløs pipeline.

A Tour of Go - Concurrency introduserer channels med kode som summerer tall med to goroutines. Sammenlign med min implementasjon i UNIX shell:

#!/bin/bash
set -e

sum() { c=$1; shift
    sum=0
    for v in $@; do
        sum=$(( sum + v ))
    done
    echo $sum > $c # send sum to c
}

main() {
    s=(7 2 8 -9 4 0)

    c=/tmp/chan$$ guard=/tmp/pipeguard$$
    mkfifo $c
    mkfifo $guard; >$c <$guard &

    n=$[ ${#s[@]} / 2 ]
    sum $c ${s[@]:0:n} &
    sum $c ${s[@]:n} &
    { read x; read y; } <$c # receive from c

    echo $x $y $((x+y))

    >$guard; wait
}

main

Jeg synes koden er veldig lik. mkfifo $FILNAVN tilsvarer navn := make(chan t).

$guard opprettes som en (inaktiv) skriver til $c for at den ikke skal lukkes for tidlig. FIFOer (named pipes) lukkes når alle skrivere er lukket (ferdige).

sum $c ${s[@]:0:n} & tilsvarer selvsagt go sum(s[:len(s)/2], c). & starter en asynkron prosess, lignende som go. Shell angir som kjent argumenter som ord, uten paranteser og komma. Slice’en sendes som et slags variadic argument, så det er mest praktisk å ha c som første parameter. (Håndteringen av array i main() er forøvrig eneste Bash-isme i koden.)

Den siste linja i main() er bare høfflig opprydding som lukker FIFOene og avventer at alle prosessene er avsluttet.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

Godt håndtverk

Jeg har akkurat funnet en skikkelig god elektriker. En som er nøye, men effektiv, passer på at det ser bra ut etterpå og at ting er logisk lagt opp (innerste lysbryter skrur av det innerste lyset). Han sa også, “jeg bruker litt ekstra tid på dette og så blir det mye enklere for neste elektriker som kommer” - og det vil fra nå være han selv. Tidligere elektrikere har resultert i synlige gule betongplugger og borring av for store hull med påfølgende akrylmasse.

Etter det kom jeg til å tenke på min egen jobb og jeg fikk fornyet motivasjon til å forsøke å utføre et godt håndtverk. Jeg mener forøvrig at godt håndtverk ikke bare gjelder for det vi kaller håndtverkere og programmerere, men også om du f. eks skriver et strategidokument eller lager et regneark.

Så, hva er det jeg prøver å gjøre? Hovedsakelig gjelder det å etterlate koden lik, eller litt bedre, enn den var når man kom dit så det vil være lettere for neste mann, som antakeligvis er meg selv.

Det gjør også at det er lettere å ta de snarveiene man noen ganger må ta for å få ting ut når det noen ganger haster.

Fart i utvikling?

Utviklingsfart er noe det ofte fokuseres på, men som jeg synes er vanskelig.

Jeg har ikke møtt noen som mener at flest antall kodelinjer per time er en god metrikk for produktivitet.

Likevel føler jeg litt den samme tilnærmingen med antall “saker” løst.

Dette kan jeg også føle på indirekte i standup-runden med spørsmålet “hva har du gjort i dag/går?”

Avhengig av hva man jobber med er det lett å føle at “Mari & Per er mye mer produktive enn meg, de shipper jo saker i ett kjør”.

Selvfølgelig gjelder jo fortsatt kvalitet > kvantitet. (Jeg tar meg i å stadig oftere helle i retningen av at all ny kode er en “byrde”, det er viktigere at du skriver den riktige koden, enn at du skriver kode.) Apropos: I fysikk skiller man mellom fart og hastighet, der sistnevnte har retning. Høy utviklingsfart er ikke alt :)

Men tilbake til Mari & Per. Jeg hørte nylig en podcast der en forfatter nevnte at det er teamets fart som er interessant. Hvert individs fart er ganske irrelavant. Så hvis du bruker en hel eller halv dag på å gjøre en skikkelig god code review, eller å lære opp en kollega i noe nytt, så er det kjempenyttig for teamets funksjon som helhet.

Ikke veldig ny kunnskap dette, men en fin påminnelse når man i standup føler man “bare” har gjort “admin”-ting den siste tiden :)

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

Zooming

De siste dagene har jeg tenkt litt på hvordan jeg bruker zooming i programmering.

Jeg liker å skrive tester før jeg skriver implementasjon. Av og til merker jeg en slags ubalanse hvor feks testene jeg skriver ikke henger så godt sammen med implementeringen. Eller at implementeringen er vanskelig. Da har jeg lært meg til å se etter et “mindre” problem å løse ved å enten zoome inn eller zoome ut.

Zoom ut

Jeg kan prøve meg på et tenkt eksempel:

Se for deg at du vil lage en funksjon som tar inn en liste med tall, og returnere en ny liste som er som input-listen—men alle tallene er lagt sammen med tallet til venstre for seg selv ganget med tallet til høyre for seg selv. Se for deg at du skriver litt eksempeltester, og det går greit til å begynne med, men etter hvert som eksemplene blir mer kompliserte blir implementasjonen vanskeligere. Helt til du innser at inni implementasjonen din ligger det en litt ræva reduce-implementasjon. (Her later vi som programmeringsspråket ditt ikke har noen reduce fra før.) Nice! Da kan du zoome ut.

Lag en ny fil/modul/klasse hvor du skriver tester for og implementerer reduce-en din uten støyen fra de obskure detaljene i den originale oppgaven din. Når du er fornøyd (nok), kan du zoome inn igjen og fortsette på den originale oppgaven.

Zoom inn

Flippen er at du kanskje så at “legge sammen med et tall og gange med et annet tall” er én ting du gjør mange ganger i problemet ditt. Så hva er enklere enn å gjøre én greie med mange ting? Det er å gjøre én greie med én ting! Så da kan vi zoome inn og lage en ny funksjon feks, som legger sammen et tall med et annet tall ganger det med et tredje tall. Zoom ut til originalproblemet ditt når du er ferdig.

Byggetid og utviklingsmiljø

Hvor mye har byggetiden til utviklingsmiljøet å si for produktiviteten?

Vi har en backend i Rust som etterhvert har blitt ganske treg til å bygge - også loopen som kjører fra man har skrevet kode til man får eventuelle feilmedlinger går også for sakte. Rust er kjent for å være raskt når det først er bygget, men kan bruke til gjengjeld ganske lang tid å bygge.

Det som gjør at man fort blir sittende med lang byggetid er at det kan være ganske krevende å finne ut av akkurat hva som gjør at ting tar tid. Er det et bibliotek man bruker eller er det noe man selv har gjort som kunne vært optimalisert?

Uansett, det værste med at byggetiden trekker ut er at man kommer at av flowen man ofte har når man får konsentrert seg om en utviklingsoppgave.

Også det å skulle kjøre for eksempel TCR vil være helt umulig.

AWK detaljert detalj

Det er overraskende vanskelig å finne nedlasting-størrelsen av et Docker-image, men noen skrev at man kan bruke manifest inspect:

docker manifest inspect -v $image | awk '/"size":/{s+=$2}END{print s/1024/1024 " MiB"}'

Her avhenger jeg av at "size": 123 er på hver sin linje, noe som ikke er urimelig.

Men jeg ble også nysgjerrig på: her summeres 123, uten problemer. Hvor trygt er det? Til og med dette virker:

echo "1432kroner" | awk '{ print int($1/100) " hundrelapper" }'
14 hundrelapper

Spesifikasjonen har noen kompliserte regler for om feltverdien tolkes som tekst, tall eller numeric string. Det viser seg at det ikke er så viktig, fordi +-operatoren alltid er Numeric, og:

the value of an expression shall be implicitly converted to the type needed for the context in which it is used. A string value shall be converted to a numeric value either by the equivalent of the following calls to functions defined by the ISO C standard:

setlocale(LC_NUMERIC, ““); numeric_value = atof(string_value);

Funksjonen atof er spesifisert som:

The call atof(str) shall be equivalent to: strtod(str,(char **)NULL),

og strod:

decompose the input string into three parts:

  1. An initial, possibly empty, sequence of white-space characters (as specified by isspace())

  2. A subject sequence interpreted as a floating-point constant or representing infinity or NaN

  3. A final string of one or more unrecognized characters, including the terminating NUL character of the input string

Then they shall attempt to convert the subject sequence to a floating-point number, and return the result.

Å lese spec’en er tidvis tungt siden det er så tett knyttet til C-koden.

Men vi kan konkludere: Det er trygt å anta at alle ukjente tegn etter tallet blir ignorert :-)

P.S. Du vil kanskje filtrere docker manifestplatform.architecture.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

JSON i Postgres

Jeg bruker mye JSON (egentlig JSONB som er mer effektivt) i Postgres og det er både raskt og enkelt å endre, men må brukes med måte.

Når vi laget designverktøyet vårt gikk i utgangspunktet alt av data fra frontenden og rett gjennom backenden som JSON og inn i en tabell i postgres. Det fungerte overraskende bra og gjorde at utviklingen av frontenden ikke trengte å stoppe opp på grunn av backend-oppgaver. Det gjorde frontend (JS/React) utviklerne mer autonome uten at de trengte å lære seg alt av backenden (Rust).

Etterhvert, når man begynner å få litt data som ikke bare kan slettes kan man begynne å savne gode gamle database-skjemaer, spesielt når det gjelder database-migreringer er det godt å kunne lene seg på noen skjemaer.

Som nevnt har vi Rust som backend og den bestemmer hva som får komme i JSON-kolonnene og ikke, det fungerer veldig bra sammen med sqlx som vi bruker til å gjøre spørringer mot databasen. For å skrive JSON til databasen må det ha en egen struct:

struct Image {
    name: String,
    alt: Option<String>,
}

og spørringen blir derfor

let image = Image {name: "123", alt: None };
let yarn_id = 1;

sqlx::query!(r#"
    UPDATE yarn
    SET image = $1
    WHERE id = $2
"#,
sqlx::types::Json(image) as _,
yarn_id
)
.execute(db)
.await?;

Her må vi wrappe image i en Json type fra sqlx.

For å hente data ut gjør jeg

struct YarnQuery {
    image: sqlx::types::Json<Image>
}

sqlx::query_as!(r#"
    SELECT image as "image: sqlx::types::Json<Image>"
    FROM yarn
    WHERE id = $1
"#, yarn_id)
.fetch_one(db)
.await?;

Vi ser at sqlx trenger litt hjelp for å vite hvilke type det er når vi setter inn JSON i as.

Når det gjelder performance så har postgres støtte for å lage indekser på verdier inne i en JSON-kolonne, uten at vi har hatt bruk for det enda.

Objektiv trynefaktor

Jeg leste nylig halve Mark Seemans bloggpost, Are pull requests bad because they originate from open-source development?.

Jeg antar at den siste halvdelen er argumentasjon for hvorfor det kanskje ikke stemmer at pull requests er bad fordi de kommer open-source-utvikling. Men som sagt har jeg bare lest halve.

I første halvdelen gjør Mark sitt ytterste for å stålmanne argumentasjonen han argumenterer mot. Først ved å prøve å gjengi originalargumentet så nyansert som mulig. (Denne teksten handler ikke om det.) Så ved å flagge sin egen predisposisjon og forutinntattheter. (Denne teksten handler om det.)

Mark forteller om hvordan han fortrekker pull requests over parprogrammering eller gruppeprogrammering. Han anser seg selv som introvert og mostly prefer solo activities. Han forteller om hvordan han foretrekker å jobbe hjemmefra, hvor han får plenty anledning til dyp fokus. Til forskjell fra å jobbe på et bråkete kontor, med lang pendleavstand. Han føler han får stort utbytte av fleksibiliteten han får av å jobbe ansynkront.

Der stoppet jeg å lese fordi jeg begynte å tenke.

Hvor mange av mine “profesjonelle” meninger om hva som er bra og hva som er dårlig er egentlig personlige preferanser? Det er sikkert ikke så vanskelig å selektivt velge seg overbevisende argumenter for hva enn preferanser man helst så at var sanne.

Jeg er dårlig til å fullføre ting, så for meg er det veldig nyttig å bryte oppgaver ned i veldig små steg, som alle gir litt og litt verdi. Det er en slags personlig work-around for min personlighetstype — men også et råd jeg gir andre i øst og vest med en slags implikasjon om at det i de fleste tilfeller er bedre enn å ta større steg. Min personlige preferanser stemmer nokså godt med hva som er ansett som god praksis av toneangivende stemmer i min boble.

Men det kan jo være at de i min boble bare har samme personlighetstype som meg. Og at de gode almenne rådene deres egentlig bare er ting som har fungert for dem. Også har de kanskje korrelerende personlighetstrekk som gjør at de ender opp som toneangivende.

Det kan også være at min boble også er et resultat av selektivitet. At jeg har endt opp i min boble ved å lytte til folk som sier ting jeg allerede er enig i. At det finnes konkurrerende, like gyldig bobler for en hver personlighet.

Hvem vet? Jeg tror i hvert fall ikke at min “jeg blir mindre distrahert av å jobbe sammen med noen” har en moralsk upper hand over “jeg er mer fokusert når jeg jobber alene”.

Go slice size bug og søk

Jeg innførte nylig en bug i prod:

parentIDs := make([]string, len(parents.els))
for _, el := range parents.els {
    parentIDs = append(parentIDs, el.Id)
}

Den første linja skulle hatt make([]string, 0, len(parents.els)), slik at kapasiteten blir satt til det siste argumentet, men lengden blir satt til 0. Dette fordi append legger til elementer på slutten av slice-en.

Jeg gjorde så et søk i kodebasen etter flere tilfeller:

find . -name \*.go -exec \
    awk '/= make\(\[\][^,]*,[^,]*$/ {    # linjer med "= make([]" og ett komma
        if ($1 == "var")                 # hvis 1. ord er "var":
            for (i=1;i<NF;i++) $i=$(i+1) # dropp første ord (som `shift`)
        if ($4 != "0)")                  # hvis siste arg != 0:
            v = $1                       # sett 'v' til variabelnavnet
    } v && $0 ~ (v " = append\\(" v) {   # hvis vi finner "v = append(v":
        print FILENAME, NR, $0           # skriv ut filnavn, linjenr og linje
    }' {} \;

Det er ikke perfekt, men det er ganske bra.

P.S. To av AWK-linjene kan droppes ved bruk av $NF i stedet for $4.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

OLORM-9

I prosjektet nå bruker vi GitHub Actions til å bygge Docker image av Go-app.

Det er litt kjedelig å endre Go-versjon fra f.eks. 1.19 til 1.20 mange (> 1) steder.

I Dockerfile kan vi enkelt bruke ARG/--build-arg:

ARG GO_VERSION

FROM golang:${GO_VERSION}-alpine

I GitHub .workflow kjører vi også noen tester med:

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v3
        with:
          go-version: 1.20

Jeg ønsker å bruke samme versjon begge steder, definert ett sted.

Helt siden Go 1.12 har go.mod inneholdt Go-versjonen.

Dette formatet er så enkelt at det ikke er vanskelig å hente ut direkte:

Each line holds a single directive, made up of a verb followed by arguments.

 <go.mod tr -s ' \t\r' ' ' | sed -n '/^ *go [1-9]/s/^ *go //p'
#└┬────┘ └┬──────────────┘   └──┬──┘└─┬──────────┘└────────┬─┘
# │       │                     │     │                    │
# │       │   default ikke print┘     └ for linjer         │
# │       │                             som starter        │
# │       │                             med ' *go [1-9]'   │
# │       │                                                │
# │       │              erstatt ' *go ' med '' og *p*rint ┘
# │       │
# │       └ erstatt alle [ \t\r]+ med ' '
# └ redirect go.mod til stdin

Men Go hjelper oss også:

The go mod edit command provides a command-line interface for editing and formatting go.mod files, for use primarily by tools and scripts.

go mod edit -json | jq -r .Go

Siden denne kommandoen er laget for akkurat dette formålet tenker jeg den er veldig trygg å basere seg på.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

UPSERT

I dag hadde jeg behov for å gjøre en UPSERT i databasen min, det vil si å gjøre en INSERT, men hvis det allerede finnes i databasen gjør vi en UPDATE på det som vi har i stedet.

Jeg har en tabell som holder oversikt over hvilken organisasjon en oppskrift tilhører, den er ganske enkel med en organization_id og en pattern_id.

CREATE TABLE organization_patterns (
    organization_id INTEGER NOT NULL,
    pattern_id INTEGER UNIQUE NOT NULL,
    PRIMARY KEY (organization_id, pattern_id)
);

Her er pattern_id satt som UNIQUE, det vil si at den kun kan finnes i en rad i tabellen og jeg kan derfor gjøre en UPDATE hvis jeg prøver å gjøre en INSERT på en pattern_id som allerede finnes. Altså at jeg bytter organisasjon til oppskriften i stedet for å legge den til en organisasjon for første gang.

sql-spørringen min for å sette inn/oppdatere blir da:

INSERT INTO organization_patterns
    (organization_id, pattern_id)
VALUES (<organization_id>, <pattern_id>)
ON CONFLICT (pattern_id)
DO UPDATE SET organization_id = <organization_id>

Til slutt så tenker jeg at kanskje organization_id bare skulle vært et eget felt i pattern tabellen og alt hadde vært litt enklere?

OLORM-7: Gjør det vondt? Lag en subkommando.

Hei!

Jeg (i dag Teodor) synes det er viktig å ta eierskap til egen arbeidsprosess for oss som jobber med utvikling. Ofte ender jeg opp med å bygge meg et CLI for å gjøre ting lett å jobbe med. I dag vil jeg dele et konkret eksempel.

Hopp forbi Digresjon: litt Clojure-koding hvis du bare vil lese konklusjonen.

Digresjon: litt Clojure-koding

Dette gjorde jeg i praksis i dag. Fram til nå har jeg trukket neste OLORM-forfatter sånn:

  1. Les oppover i olorm-intern etter hvem jeg trakk forrige gang

  2. Skriv en ny Clojure-kodesnutt for å trekke neste OLORM-forfatter. For eksempel, hvis forrige OLORM-trekking var følgende:

    $ bb -e "(rand-nth '(oddmund lars richard))"
    oddmund

    Så kan neste trekking bli sånn:

    $ bb -e "(rand-nth '(lars richard))"
    richard

    (når vi “går tom”, fyller vi på med Lars, Richard og Oddmund i trekke-sekken igjen)

I dag synes jeg dette var kjipt. Det var mye skriving. Så jeg bestemte meg å ta eierskap for det som gjorde vondt ved å skrive en subkommando.

Jeg startet med å skrive ned hvordan jeg ville subkommandoen skulle fungere:

$ olorm draw olr
richard

Det er viktig for meg å tenke på hvilken oppførsel jeg ønsker før jeg begynner å kode. Ellers føler jeg at jeg bare surrer rundt. Og når jeg har ett eksempel på hva jeg vil at skal funke, kan jeg implementere akkurat det, uten å skrive masse kode jeg må kaste.

Jeg starter med å lage en ny subkommando som gjør at jeg kan se hva jeg driver med:

$ git diff f693631..3abc617
diff --git a/cli/src/olorm/cli.clj b/cli/src/olorm/cli.clj
index 27da7d5..3a1f0e1 100644
--- a/cli/src/olorm/cli.clj
+++ b/cli/src/olorm/cli.clj
@@ -77,12 +77,21 @@ Allowed options:
           (shell {:dir repo-path} "git commit -m" (str "olorm-" (:number olorm)))
           (shell {:dir repo-path} "git push"))))))

+(defn olorm-draw [{:keys [opts]}]
+  (let [pool (:pool opts)]
+    (prn `(str/blank? ~pool)
+         (str/blank? pool))
+    (prn `(rand-nth ~pool)
+         (rand-nth pool))
+    ))
+
 (def subcommands
   [
    {:cmds ["create"]        :fn olorm-create}
    {:cmds ["help"]          :fn olorm-help}
    {:cmds ["repo-path"]     :fn olorm-repo-path}
    {:cmds ["set-repo-path"] :fn olorm-set-repo-path :args->opts [:repo-path]}
+   {:cmds ["draw"]          :fn olorm-draw          :args->opts [:pool]}
    {:cmds []                :fn olorm-help}])

 (defn -main [& args]

Den kan jeg bruke sånn:

$ olorm draw olr
(clojure.string/blank? "olr") false
(clojure.core/rand-nth "olr") \l
$ olorm draw
(clojure.string/blank? nil) true
(clojure.core/rand-nth nil) nil

(digresjon: jeg synes REPL i Clojure er fantastisk, men når jeg skriver CLI-er foretrekker jeg å jobbe direkte med CLI-et)

Så:

  1. clojure.string/blank? kan si meg om jeg har en tom tekststreng, og gir samme svar når den får inn "" og nil. (nil i Clojure er som null eller undefined i Javascript)
  2. Men rand-nth bare gir meg nil hvis jeg prøver å trekke fra en tom liste. (og Clojure later som at nil er en “tom collection av passende type”. Dette fenomenet kalles nil-punning)

Jeg tenker å trekke personens navn fra et map sånn:

user=> (get (zipmap "olr" '(oddmund lars richard)) \r)
richard

zipmap tar inn to “ting som ser ut som lister” og lager en mapping fra elementer i den venstre lista til elementer i den høyre lista.

Så jeg implementerer kommandoen sånn:

(defn olorm-draw [{:keys [opts]}]
  (let [pool (:pool opts)]
    (prn
     (get (zipmap "olr" '(oddmund lars richard))
          (rand-nth pool)))))

Meeen, det er vanskelig å lære hvordan man bruker kommandoen.

$ olorm draw olr
oddmund
$ olorm draw
nil

Så jeg vil ha hjelpetekst. Da skriver jeg et eksempel på hjelpeteksten jeg ønsker:

$ olorm draw
[skal returnere exit-kode 1]
???

Jeg ser på en hjelpetekst som finnes:

Usage:

  olorm create [OPTION...]

Allowed options:

  --help               Show this helptext.
  --disable-git-magic  Disable running any Git commands. Useful for testing.

OK, nå var det lettere.

$ olorm draw
Usage:

  olorm create POOL

POOL is a string that can contain the first letters of the OLORM authors.
Example usage:

  $ olorm draw olr
  Richard

Nå har jeg skrevet helptext som funker (mener jeg) i rett kontekst.

Så jeg implementerer helptext i kommandoen:

(defn olorm-draw [{:keys [opts]}]
  (let [pool (:pool opts)]
    (when (or (:h opts)
              (:help opts)
              (not pool))
      (println (str/trim "
Usage:

  $ olorm create POOL

POOL is a string that can contain the first letters of the OLORM authors.
Example usage:

  $ olorm draw olr
  Richard
"
                         ))
      (if (or (:h opts) (:help opts))
        (System/exit 0)
        (System/exit 1)))
    (prn
     (get (zipmap "olr" '(oddmund lars richard))
          (rand-nth pool)))))

Den kan brukes sånn:

$ olorm draw
Usage:

  $ olorm create POOL

POOL is a string that can contain the first letters of the OLORM authors.
Example usage:

  $ olorm draw olr
  Richard

… og kommandoen kan brukes neste gang:

$ olorm draw olr
lars

meeen det teller ikke, vi skal trekke på ekte i morgen.

Oppsummering

  1. Når noe gjør vondt i utviklingsprosessen min, prøver jeg å løse det ved å lage meg CLI-er jeg kan bruke.
  2. Jeg tillater meg selv å kose meg litt når jeg gjør det. Jeg mener også det er en del av jobben vår å sørge for god developer experience når vi koder.
  3. Hvis ingen tar ansvar for ergonomien i det vi koder, kommer det til å bli bare dritt. Så er vi i gang. The Pragmatic Programmer har et kapittel som heter “Don’t leave broken windows” om dette.

Retrospektiv

  1. OLORM-er skal ta 5-10 minutter å skrive. Jeg sprengte tidsskjemaet med cirka ti-gangeren. Dette setter dårlig presedens, og jeg innser at det er kjempevanskelig å vise “en liten bit uten å bruke masse tid.”
  2. Det var litt gøy å skrive :)
  3. Jeg er glad vi har et CLI for OLORM så vi kan fikse småting som “å trekke OLORM-forfatter er kjedelig”
  4. Hvis man skal skrive så langt som dette hver gang, er hver tredje dag alt for ofte. Jeg kunne kanskje satt av tid til en sånn en hver uke. Eller annenhver uke. Men jeg kunne jo også gjort noe “mindre”. Feks trukket en tilfeldig personlig aforisme, og forklart hva jeg legger i den.

Referanser

GeePaw Hill er en flink fyr som også snakker om hvordan vi utviklere kan ta kontroll over arbeidsprosessen vår. Hvis du synes denne artikkelen var spennnede, vil du kanskje like podcasten hans.

Test tidlig, mens det fortsatt gjør litt vondt

Det er lett å utsette å teste det man lager på ekte brukere, det er alltid en til feature som man ønsker å ha ferdig før det er “klart”. Men man må slippe det man har laget løs mens det fortsatt føles litt tidlig og er litt ubehagelig.

I dag hadde vi en test på et design-verktøy uten blant annet innlogging, lagring og bytte av farger. Vi ville først og fremst test brukbarheten til verktøyet. Vi har en lang liste med features og fikser som vi trenger å få på plass, men tilbakemeldingene vi fikk fra brukerne våre var nesten utelukkende andre ting som vi ikke hadde tenkt så mye på. Det gjør at vi nå i mye større grad lager det viktigste først.

Vi får fokuset bort fra “en til feature” til å gjøre det man har solid og brukervennlig.

Semikolonfri Rust

Hvordan ville Rust-koden vår sett ut uten semikolon?

Jeg tror dette kunne vært et interessant eksperiment.

Navnet er inspirert av point-free style (PFS), uten noe mer felles enn at både SFR og PFS er paradigmer/dogmer for hvordan man skriver en del av koden sin.

Go Single Method Interface og Adapter

(Tilsvarer Kotlin og Javas Single Abstract Method (SAM) interface / functional interface)

Grensesnitt med en enkelt funksjon er veldig praktiske:

type Greeter interface {
    Greet(name string) string
}

De kan enkelt sendes som parametre eller returverdier til og fra funksjoner (høyere ordens funksjoner), de kan komponeres med andre interfaces, og de er relativt enkle å implementere.

Man kan dessverre ikke (ennå hvertfall) implementere en SMI (Single Method Interface) direkte fra en funksjon, men man kan lage et Adapter. Vi lager først en funksjonstype med samme signatur som interface-funksjonen:

type GreetFunc func(string) string

og deretter implementere interfacet på denne:

func (f GreetFunc) Greet(name string) string {
    return f(name)
}

Nå kan vi hvor som helst enkelt implementere Greeter fra en funksjon:

func main() {
    greeter := GreetFunc(func(s string) string {
        return fmt.Sprintf("Hello, %s", s)
    })
    greeting := greeter.Greet("Bob")
    fmt.Println(greeting)
}

Du har sannsynligvis brukt denne teknikken allerede vba. http.HandlerFunc.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

En hyllest av JSON

XML og YAML har noen nyttige funksjoner som JSON ikke har, men har også mye mer kompleksitet.

JSON er så lite komplisert at spesifikasjonen kun består av fem enkle diagrammer.

Dette gjør at jeg nettopp kunne skrive et enkelt testscript helt uten noen JSON parser:

curl -s 'http://address' | tee tmp/data.json
if tr -d ' \t\n\r' < tmp/data.json |
    grep '"type":"redirect"' |
    grep '"action":301' |
    grep '"redirectUri":"https://address"'
then
    echo 'Test OK'
fi

Kjernen av koden er kommandoen tr -d som fjerner alle lovlige whitespace-tegn fra JSON-dataen og slik normaliserer den. (Merk at whitespace inni tekststrenger også fjernes, som kan være problematisk i andre tilfeller.)

Denne koden (med unntak av curl) er en gyldig POSIX shell-kommando og vil derfor virke på alle CI-systemer etc. i all overskuelig fremtid.

Send gjerne spørsmål eller kommentarer til Richard Tingstad :)

OLORM-2

Nå har jeg nettopp wipet rent worktree-et mitt med git reset --hard origin/main.

Jeg kasta arbeid fordi jeg har lært noe. Jeg ville endre på noe i et Rust-prosjekt. Jeg dro den interessante koden ut til en ny funksjon jeg kunne teste. Jeg lagde en eksempeltest som reproduserte feilen. Jeg lagde en propertytest som testet at feilen “aldri” ville oppstå.

Så begynte jeg å implementere en løsning. Men ble bitt litt av forventninger rundt hva jeg kan gjøre med dynamisk formatering av strenger.

Jeg er der i min Rust-reise at jeg stort sett klarer å få ting gjort, men jeg er ikke der at det føles riktig. Jeg vil ofte løse ting på måter som Rust enten gjør umulig (såvidt jeg vet) eller tungvint.

Jeg spurte Marcus om råd, og han foreslo å lage typer og implementere traits. Jeg tenkte at det var veldig omstendelig for å implementere den lille greia jeg holdt på med.

Men etter litt demonstrering tror jeg han har rett.

Å lage en type og implementere én trait vil ikke egentlig supplere min funksjon, men erstatte den. Semantikken i hva jeg prøver på og property testene vil også være lettere å lese og gi mer mening om jeg lager en type til dette. Og jeg tror det er lettere å finne et naturlig sted for koden å bo om dette er en egen type.

Jeg har lest at det er en vanlig misforståelse å tenke på Rust som et funksjonelt språk. Og det er nok en feil jeg gjør en del.

Jeg tror jeg vil få mer ut av å lene meg inn i typene til Rust. Med et lett dryss av objektorientering, kanskje.

En god natts søvn

I går holdt jeg på en litt kompleks refaktorering hvor jeg skal hente fargevarianter for en garnpakke fra to forskjellige steder i databasen, avhengig av om de er opprettet av designeren av oppskriften eller strikkeren har laget sin egen fargevariant. Det endte med at jeg ikke ble ferdig før jeg måtte gå hjem, og jeg så for meg at jeg trengte å bruke et par timer på det i dag. Hadde jeg blitt på jobb hadde jeg nok også lett brukt et par timer.

Når jeg kom på jobb i dag så jeg umiddelbart hva som var feil, et lite stykke unna der jeg jobbet i går, og fikset alt på ca 5 minutter.