Fork me on GitHub
#clojure-norway
<
2023-06-09
>
slipset07:06:31

Hmm, noe i den tråden mellom @leif.eric.fredheim og @christian767 om @leif.eric.fredheim’s babashka adventur klikka i morges. I Clojure så er det veldig lett å gjøre:

(assoc foo :frobnitz (gazoonk whatever))
Og det er fint. Men i blandt, som @leif.eric.fredheim viste igår, så er det ikke alltid åpenbart hvorfor foo trenger akkurat en frobnitz akkurat her. Så da skriver man en kommentar:
;; the foo needs a :frobnitz to be able to initalize the frobnitz handler
(assoc foo :frobnitz (gazoonk whatever))
Men! Dette kan veldig godt være kodebasen din som forteller deg at det å legge til en frobnitz er en viktig del av domenet ditt og dermed fortjener et top level navn dvs en fn med tilhørende doc string

slipset08:06:13

Jeg har i så måte blitt veldig glad i noe som man skulle kunne si ligner på getters/setters i Clojure

slipset08:06:40

Og bare sånn at ikke @christian767 og @magnars får lunch-kaffen helt i vrangstrupen. Jeg synes kodebasen blir lettere å navigere (og lese) dersom man rett i nærheten av der man gjør f.eks:

(:frobnitz foo)
har en
(defn set-frobnitz [whatever v] (assoc whatever :frobnitz v))
gjerne med en docstring (og et bedre navn) slik at man lett kan se hvor i kodebasen man faktisk setter frobnitzer heller enn å måtte søke etter :frobnitz og håpe på det beste. Gitt namespace’a kw med doc-strings ville man sikkert oppnådd mye av det samme
(def frobnitzer "Does what any good old frobnitzer would do" ::my-ns/frobnitz)

👍 2
cjohansen08:06:52

Interessant 🙂 Jeg setter ikke kaffen i vrangstrupen, ser nytten.

👍 2
cjohansen08:06:15

Selvom jeg normalt sett blir litt lei meg når jeg må lage en funksjon som tar imot et map og bare gjøre assoc eller update

slipset08:06:56

(defn versioning-ctx
  "Set a parameter in `ctx` that tells the postgres-repo mutation functions that care that they need to worry
  about versioning."
  [ctx]
  (assoc ctx :suppress-versioning? false))

cjohansen08:06:16

Ofte er løsningen at man heller lager en funksjon for å beregne verdien, og beholder assoc/`update` på kallstedet. Men noen ganger er selve settingen som du sier viktig nok til å “løfte opp”

cjohansen08:06:19

Ja, ikke sant

cjohansen08:06:19

Jeg ville kalt den funksjonen get-versioning-ctx - har fått kjeft av @magnars for funksjoner med substantiv-navn mange nok ganger til å være enig i at det er bedre med et verb 😄

😅 4
magnars08:06:32

*set

😅 2
cjohansen08:06:57

jeg tygde litt på det, kan klare å argumentere for begge

magnars08:06:03

den gjør jo assoc, så "get" er det ikke

cjohansen08:06:20

“Jeg har denne konteksten, gi meg en lik en som er versjonerende, takk”

slipset08:06:29

Jeg bruker konsekvent ikke set fordi jeg ser for meg at @magnars en eller annen gang blir innleid som konsulent i Ardoq…

magnars08:06:03

assoc er greit, set er greit, include er greit, men det e'kke get! 😅

magnars08:06:32

jeg kan til nøds godta get-with-

cjohansen08:06:35

makan. her har jeg lært meg å unngå en type kjeft bare for å løpe rett i inn i en annen 😄

😂 6
magnars08:06:00

jeg ser det nå

magnars08:06:09

man har introdusert noe som heter "versioning-ctx"

magnars08:06:22

men ja, greit, da er get fint

cjohansen08:06:51

du gir den en ctx og får en versioning ctx tilbake. da er get helt fint

msolli08:06:57

Jeg har lagt meg på en konvensjon med ->-prefiks når en funksjon transformerer input til noe annet: ->versioning-ctx.

magnars08:06:04

jeg ville foretrukket mindre "kingdom of the nouns"

slipset08:06:07

Tenk å leid inn dere to til en ukes code-review.

❤️ 2
cjohansen08:06:23

@U06BEJGKD ja, jeg bruker også -> når det er transformasjoner

cjohansen08:06:46

men eksempelet til Erik legger til informasjon, så det er ikke en ren transformasjon i mine øyne

msolli08:06:25

Nja, et map transformeres til at annet map, i dette tilfellet ved å legge til noe. 🤷

slipset08:06:59

Man kunne jo også set for seg noe som:

(def versioning {:suppress-versioning? false})
og så kunne man hatt: (assoc ctx whatever/versioning)

cjohansen08:06:52

Må bli merge eller into i så fall, men ja - absolutt en mulighet

cjohansen08:06:42

Skal vi prate litt om den doble negativen her eller? 😅

slipset08:06:12

Nei, det skal vi ikke

😂 4
cjohansen08:06:39

(defn enable-versioning [ctx]
  (assoc ctx :versioning? true))

(defn suppress-versioning [ctx]
  (assoc ctx :versioning? false))

😍 2
magnars08:06:19

Der skjedde det mye fint på en gang.

slipset08:06:34

Kunne da tilogmed våget seg med en (defn disable-versioning [ctx]…)

slipset08:06:51

Så hadde navngivingen blitt litt symmetrisk.

cjohansen08:06:05

Sant! Jeg syns bare at “suppress” var så elegant 😄

😂 2
cjohansen08:06:43

Men det har vel ikke et direkte antonym, så enig med deg

cjohansen08:06:01

con-versioning / sin-versioning 😄

magnars08:06:14

Deilig å bli kvitt både dobbel negativ og frigjøre seg fra kingdom of the nouns i en liten forandring.

slipset08:06:27

Men, uansett, jeg synes hovedpoenget holder. Av og til er det verdt å navngi og løfte opp operasjoner som egentlig bare er en assoc fordi det gjør kodebasen enklere å lese/navigere/forstå

mariuene08:06:51

versioning / versionnot

😁 2
magnars08:06:04

Altså, uten docstrings, så syns jeg fortsatt det er meningsløst. Men som en "docstring receptacle" så syns jeg det er helt fint.

slipset08:06:45

Og det gir rom for å utvide senere. Det gir et level of indirection eller et interface, om du vil.

magnars08:06:04

Hvis du trenger å utvide senere, så kan du lage det rommet når du trenger det. 😅

slipset08:06:42

Det kan du si, men da er det kjedelig å løpe rundt på alle steder jeg har assoc :foo bar og legge til assoc :qux bla

magnars08:06:27

Bygg det du trenger nå. Som det heter: Det er vanskelig å spå, særlig om framtiden.

slipset08:06:39

Men, men, men et poeng til 🙂

slipset08:06:06

Hvis vi ikke hadde hatt denne funksjonen, så måtte man (gitt dårlige docstrings) lest implementasjonen for å skjønne at hvis man ville gjøre noe med hvordan vi versjonerte ting så kunne man legge til :suppress-versioning? falsectx Nå ser man fra top-level defs at det er en av kapabilitetene til dette ns’et

magnars08:06:42

Ja, kan være en ålreit måte å eksponere et API på.

slipset08:06:35

og så slipper man å huske på at her er det jammen dobbelt negering som gjelder, og at det heter suppress og ikke disable

magnars08:06:04

akkurat det burde jo heller ryddes opp i, vil jeg si, men greit nok

mariuene08:06:09

hvorfor ikke flytte det et lag over

(defn update! [ctx thing]
  (foobar-db ctx thing))

(defn skip-version-update! [ctx thing]
  (-> (skip-versioning-ctx ctx)
      (update! thing)))
Da slipper man å måtte være klar på at man må oppdatere ctx for å skippe/enable versioning?

teodorlu08:06:44

Hallo aus Berlin!

👋 4
cjohansen08:06:20

Forslaget til @marius.enerly oppnår også å flytte informasjonen om versjonering til der du skal committe, altså nærmere der det har noe å si 👌

magnars08:06:01

Ja, det var fint. 🙂

cjohansen08:06:05

Men jeg sier meg også enig i poenget til @slipset: Noen ganger er det ålreit å løfte rett frem datamanipulering inn i en funksjon, som en slags reklameplakat.

magnars08:06:41

som reklameplakat og docstring-receptacle 👍 utover denne bruken bør man i høyeste grad unngå å ha assoc som første form i en funksjon.

magnars08:06:09

besørg også at reklameplakaten din ikke gjøre noe annet enn assoc.

magnars08:06:23

Jeg vil ikke skrive

(:foo (assoc-foo {} params))

cjohansen08:06:43

Nei, da er det game over

augustl09:06:29

henger meg litt opp i navnene her og jeg egentlig. Hva med litt namespaced keys sånn at navnet gir mening på egne bein? Det er ihvertfall en annen approach til problemet “hva i alle dager beryr :frobnitz i denne konteksten”

👌 2
leifericf09:06:39

Noob spørsmål her… Jeg synes det er fint å bruke -> og ->> for å lage "pipelines" av funksjoner. Men det er noen ganger vanskelig, fordi enkelte funksjoner forventer første parameter, og andre forventer siste parameter. Da står jeg litt fast, fordi det er enten-eller med -> eller ->>. Finnes det en "thread-first-or-last" macro også? :thinking_face: Eller må jeg lage en liten "wrapper-funksjon" for å endre rekkefølgen på paramenterne til "inkompatible" funksjoner? Jeg ser det finnes cond-> men er ikke helt sikker på om den kan brukes til noe slikt.

teodorlu09:06:49

Jeg pleier å bestemme meg for å bruke ->> eller ->, og så skrive de funksjonene som mangler. Med thread first må jeg ha funksjoner som tar tingen som første argument.

teodorlu09:06:12

Du kan eventuelt bruke as-> for å bestemme for hvert ledd!

leifericf09:06:24

Ja, det er det jeg holder på med også, men jeg trodde kanskje jeg holdt på med noe dumt.

teodorlu09:06:39

Kanskje noen mer erfarne har andre meninger :)

leifericf09:06:19

Cool, as-> var jeg ikke klar over fantes.

cjohansen09:06:19

Ikke bruk den 😅

👍 2
😄 2
cjohansen09:06:48

Koden blir veldig tung å lesr

cjohansen09:06:38

Clojure har en tydelig, men underkommunisert konvensjon: collections går i thread last, maps I thread first. Core apiet er veldig konsistent på dette

💡 4
cjohansen09:06:54

Så steg 1 er å følge denne med dine egne funksjoner

cjohansen09:06:22

Dersom du allikevel får lyst til å bytte threading: bryt opp, se om du kan plukke ut funksjoner osv

cjohansen09:06:14

cond-variantene et threading med conditionals for hvert steg, nyttig til sitt bruk

👍 2
teodorlu09:06:31

Mulig å be om et eksempel?

magnars09:06:16

Tenk at ingen har dratt fram bloggposten min enda 😅

leifericf09:06:16

Jøss, nå er jeg glad for at jeg spurte! Fantastisk blog post, @magnars.

❤️ 4
teodorlu09:06:11

Å åpne opp læringsprosessen sin har mange fordeler! ❤️❤️😊

💯 2
teodorlu09:06:59

> Istedet bryter du opp koden. Når du går fra et map til en seq, bryt opp threadingen. Når du går fra en seq til et map, bryt opp. Det er lov å koste på seg et navn. Point free programming er kult det, men ikke dra det for langt da. > Huh, jeg hadde ikke koblet threading-makroene med point free style. Men det er jo det samme, det! 😊

cjohansen09:06:33

@teodorlu dårlig eksempel på cond->:

(cond-> {}
    raining? (assoc :weather :raining)
    cold? (assoc :temps :low))

teodorlu09:06:56

Hvorfor er det dårlig?* (jeg klarer ikke se at det ser så dårlig ut)

cjohansen09:06:05

hvorfor man vil bruke den?

cjohansen09:06:13

eller hvorfor det er dårlig?

teodorlu09:06:53

Hvorfor er det dårlig?

cjohansen09:06:02

fordi jeg fant det på i farta 😂

😂 2
cjohansen09:06:04

(cond-> strategy
  hour-minute
  (assoc :rule/hour-minute hour-minute)

  (:strategy/from-ldt strategy)
  (assoc :rule/from (.atZone (:strategy/from-ldt strategy) (:zid ctx)))

  (:strategy/to-ldt strategy)
  (assoc :rule/to (.atZone (:strategy/to-ldt strategy) (:zid ctx))))

👍 2
cjohansen09:06:13

Dette er fra koden vår, så det er forhåpentligvis ikke dårlig

teodorlu09:06:11

Jaaa, dette er lurt, tror jeg. Jeg har noe kode der jeg overskriver samme variabel flere ganger i en let.

(let [
  x {}
  x (if y (assoc x :y y) x)
  ,,,]
 ,,,) 
Lukter cond->. (dumt formatert, sitter på mobilen)

💡 2
slipset09:06:32

En annen (fin) bruk for cond-> er hvis du finner at du skriver kode som

(if whatever
  (assoc foo :bar baz)
  foo)
Dette løser du med cond->
(cond-> foo
  whatever (assoc :bar baz))

slipset09:06:50

Sikkert noe som noen skulle kunne legge til kibitz

👍 4
cjohansen09:06:51

Ja, det er kanskje den beste bruken

msolli09:06:04

Her er en annen klassisk artikkel om temaet: https://stuartsierra.com/2018/07/06/threading-with-style

👍 2
slipset09:06:58

To små gotchas med cond-> 1. Du threader ikke gjennom condition’en 2. Den exiter ikke tidlig slik som cond gjør Det siste punktet betyr at hvis du har flere conditions som er true, så threades verdien din gjennom alle.

👍 2
teodorlu10:06:55

Er det her some-> kommer inn? Bruker dere den I praksis?

slipset10:06:08

some-> bruker vi ganske ofte.

slipset10:06:14

Evt some->>

cjohansen10:06:25

basically: nil-safe threading

👍 2
slipset10:06:41

Man skulle kunne tenke på some-> som en slags maybe-monad flatMap aktig greie

teodorlu10:06:46

Ja. Eller > Fordi vi har some-> og nil-punning trenger vi ikke å tenke på monader > ?

cjohansen10:06:08

Jeg skjønner meg fortsatt ikke på monader, så jeg må melde pass

slipset10:06:24

Jo, det stemmer nok, men jeg synes det er kult å se hvordan ideene dukker opp i forskjellige implementasjoner. Clojure er jo befriende fritt for applicatives, functors og alt det der, men det er jo der. Jeg tror map f.eks er en functor og at en applicative er den funksjonen du sender inn til map

slipset10:06:30

Men dette er bare ting jeg tror.

teodorlu10:06:40

Min forståelse: • Functor er ting du kan kjøre map på. Feks clojure sequences • Applicative skjønner jeg ikke helt, men tror det har noe med parallellisering å gjøre

slipset10:06:53

Der ser du.

slipset10:06:00

Hva jeg tror

2
augustl11:06:42

lurer på om grunnen til at jeg ikke skjønner monader, og grunnen til at jeg foretrekker dynamiske språk, er den samme tingen (uten at jeg har innsikt i hvorfor eller hvordan eller noe som helst)

2
augustl09:06:44

min tommelfingerregel der er at når det behovet melder seg, kan det være greit å stykke ting opp litt kanskje 🙂

👍 4
leifericf10:06:01

Gudbedre, det var nok internett for i dag 😅

slipset10:06:21

Litt off-topic, men jeg bor på hostel her på Granka, basically private rom med felles kjøkken og bad. Videre, vi er litt nervøse for at eldstesønnen på 16 skal fly til Køben og klare å surre seg fram til en fekte-camp i sommer. Og også surre seg hjem igjen. Derfor følger kona med og sørger for at han kommer seg til camp’en, og så får han komme seg hjem på egen hånd. Han skal være borte ca en uke - full kost og losji I går ramla det inn to japanere her. Den ene 16 og den andre 13. De hadde flydd 27 timer fra Japan, mellomlanda i Istanbul og Madrid. Skal være her i en måned. Lagde seg middag da de kom og kjørte på med pølser, ostesmørbrød, speilegg, salat og banan til frokost. Litt av en kontrast til mine barn.

😅 6
leifericf10:06:21

Jeg er dobbelt statsborger (norsk-amerikansk) og har flydd alene for å besøke familien i USA mange ganger siden jeg var 6 år gammel 🙂 Med ledsager fra flyselskapet da vel å merke.

leifericf10:06:06

Og fordi kona mi er japansk, er jeg over middels interessert i japansk språk og kultur. Hver gang vi reiser dit blir jeg overrasket av å se førsteklassinger reise med toget alene til skolen, osv. Barn har mye mer selvstendighet der.

augustl11:06:39

har sett søte (og sikkert sanne?) poster om hvordan det er en kultur i Japan for at alle voksne har et slags felles ansvar på å hjelpe og passe på barn som er alene

leifericf11:06:59

Ja, det er noe sant i det, og kanskje spesielt ut på landet 🙂

msolli10:06:06

Dere driver ikke med helicopter parenting, men aircraft parenting.

augustl11:06:33

in other news, gratulerer med dagen @leif.eric.fredheim 🥳 🎉

💜 2
leifericf11:06:33

Tusen takk! Hvordan fant du ut det? 😄

augustl11:06:50

jeg hadde veldig lyst til å løfte nettverkingkompetansen min når jeg søker etter jobber med noen gode tips for å skrive e-poster jeg sender til folk jeg ikke har kontakt med fra før, men så ble jeg avbrutt av linkedin med en bursdagsmelding i stedet for 😄

leifericf11:06:25

Ahaaa! LinkedIn, ja!

slipset11:06:25

Gratulerer med dagen!

💜 2
cjohansen11:06:33

Congratulatore 🥳

💜 2
slipset13:06:28

> jeg hadde veldig lyst til å løfte nettverkingkompetansen min når jeg søker etter jobber med noen gode tips for å skrive e-poster jeg sender til folk jeg ikke har kontakt med fra før Dette er nok en av de tyngre setningene jeg har lest på en stund. Ikke helt sikker på om jeg forstår hva du sier 🙂

😂 2
augustl13:06:31

burde trykket på linken ved siden av bursdagsmeldinga på linkedin egentlig, jeg ble så gira av bursdagen at jeg helt glemte å benytte meg av det flotte tilbudet LinkedIn kom med der

augustl13:06:04

her var den. Nå skal tilbudene formerlig renne inn til Oslo CRUD og Business, tenker jeg! https://www.linkedin.com/news/story/making-cold-emailing-work-5301969/

leifericf13:06:49

Nå ble det fryktelig til spam på meg i dag, men… Jeg har endelig fått mitt første Babashka script til å gjøre det jeg ville! Men det er potensielt mye dumt og/eller stygt der; rom til forbedringer. Er det noen som har tid til å ta en kjapp titt på denne for å gi meg feedback på hva jeg bør gjøre annerledes? https://github.com/leifericf/leifs-utils/blob/main/src/leifs_utils/core.clj

slipset14:06:45

Synes det ser helt nydelig ut!

slipset14:06:58

Hvis jeg skulle kommentere på noe, så er det vel at:

(process/sh "az devops project list --org" org-url)
                :out
                (json/parse-string true)
og
(process/sh "az repos list --project" project-id)
                   :out
                   (json/parse-string true)
Ligner litt mye på hverandre?

leifericf05:06:23

Ja, enig! Godt observert. Jeg tenkte også på det, men var usikker på hvordan jeg skulle fikse det. Tenkte kanskje det fantes et lurt triks med partial og comp eller defmacro, men jeg tenkte ikke på å bare lage en enkel funksjon for de delene som er helt like, slik du foreslår.

slipset14:06:51

(defn process-out->json [something other]
  (-> (process/sh something other) :out (json/parse-string true)))

👍 2
slipset14:06:02

modulo litt naming 🙂

slipset14:06:13

usikker på hvordan process/sh virker om den bare tar en gjeng med args eller om du kunne har gjort (str az repos list --project " project-id) og sendt det til process/sh

leifericf05:06:38

Tror jeg kan sende inn en streng til process/sh, ja!

slipset14:06:35

I safall kunne process-out->json bare tatt et argument.

👍 2
slipset14:06:25

Men, kanskje det aller viktigste! Dette virker bare for de første 16 (eller 32?) repo’ene 🙂

💡 2
leifericf06:06:43

Jøss! Det kan se sånn ut, ja, men jeg forstår ikke hvorfor 😅 Hvorfor prosesserer ikke map hele lista med alle repo URL-er? :thinking_face:

cjohansen07:06:24

map er lazy og realiserer chunks på 32 entries om gangen. Så om du feks ber om first så vil den mappe 32 items, men ikke mer.

😮 2
cjohansen07:06:53

På grunn av dette bør du aldri bruke map til sideeffeekter. Bruk doseq i stedet.

💡 2
leifericf07:06:59

Jøss! Takk! Det ville tatt meg lang tid å finne ut av på egenhånd når jeg omsider hadde oppdaget feilen 😅

cjohansen07:06:43

Spesielt fordi det ser ut som om det fungerer om du kjører det i REPL-et: REPL-et vil realisere collectionen for å vise den til deg 😅

💡 2
cjohansen07:06:01

Laziness er amazing til dataprossessering, men ikke egnet til side-effekter

👍 2
slipset14:06:12

(defn clone-all-repos
  "Clone all Git repos from Azure DevOps."
  [] (->> (get-all-repo-data)
          (tree-seq coll? identity)
          (keep :sshUrl)
          (map clone-repo)))
Burde nok skrives om til enten en doseq eller en run!
(defn clone-all-repos
  "Clone all Git repos from Azure DevOps."
  [] (->> (get-all-repo-data)
          (tree-seq coll? identity)
          (keep :sshUrl)
          (run! clone-repo)))

👍 2
leifericf06:06:36

run! var relativt enkelt å bruke, fordi jeg kunne simpelthen bytte ut map med run!. Men hvordan bruker jeg doseq i denne thread-last makroen? I og med at doseq trenger en vector med bindings. Må jeg pakke inn hele thread-last makroen i doseq på et vis, eller skrive om hele body uten å bruke thread-last makroen? :thinking_face:

(defn clone-all-repos
  []
  (->> (get-all-repo-data)
       (tree-seq coll? identity)
       (keep :sshUrl)
       (run! clone-repo)))

cjohansen06:06:38

run! er for map hva doseq er for for 😅

💡 2
cjohansen06:06:59

Du kan ikke threade deg inn i doseq

leifericf06:06:38

Ahaaa, okay! Skjønner. Så det har egentlig ingen praktisk betydning hvorvidt jeg bruker run! eller doseq her, fordi det er to "flavors" av samme mekanisme på en måte?

leifericf06:06:22

Så fantastisk! Jeg satt og reiv meg i håret på hvordan jeg skulle bruke doseq også har jeg allerede gjort det uvitende 😛

cjohansen06:06:08

Du ble sikkert forvirret av at jeg kun snakket om doseq, for det er den jeg pleier å ty til for side-effekter. Men det er bare tilfeldig at det har blitt sånn for meg 🙂

leifericf06:06:30

En ting jeg la merke til: Når jeg bruker map så får jeg masse output i REPL, men ikke når jeg bruker run!. Med run! får jeg ingen output som viser at det skjer noe som helst, men jeg ser i mappa at ting skjer.

leifericf06:06:25

Ja, jeg ble litt forvirret av det, og fordi @slipset nevnte begge i sin melding, så tenkte jeg feilaktig at det var to forskjellige alternativer.

cjohansen06:06:31

Det er nokså vanlig at man i funksjonelle språk skiller på retur-verdier og side-effekter. Funksjonen din bør helst ha én av dem.

cjohansen06:06:46

Så side-effekt-funksjonene i Clojure returnerer ingenting

💡 2
cjohansen06:06:02

(doseq [url (->> (get-all-repo-data)
                 (tree-seq coll? identity)
                 (keep :sshUrl))]
  (clone-repo url))

💡 2
cjohansen06:06:07

Sånn ser doseq ut, for ordens skyld

leifericf06:06:53

Ahaaa! Man må putte hele thread-last i selve bindingen, ja. Jeg glemmer at en kan gjøre mer avanserte greier inne i binding.

leifericf06:06:07

Så hvis jeg forstår rett så er run! en eager versjon av map, og doseq er en eager versjon av for i den forstand at de førstnevnte "tvinger ut" hele sekvensen uansett hvor stor den er, mens de sistnevnte evaluerer kun det som er nødvendig (avhengig av hvordan returverdien blir brukt).

cjohansen06:06:50

Ja, og hvor både run! og doseq er “for the purpose of side-effects” 🙂

👍 2
slipset06:06:32

Nå får sikkert @christian767 morgenkaffen i vrangstrupen sin, men hvis du vil ha både sideeffekter og returverdier, så er reduce (eller grøss mapv ) vennene dine.

😂 2
cjohansen06:06:09

Hvis du har lyst til å bruke mapv til side-effekter så VÆR SÅ GOD!

cjohansen06:06:19

Blir radio silence når du kommer for å få hjelp 😂

😂 2
slipset06:06:30

Jeg har ikke det, bare sier at det faktisk funker.

cjohansen06:06:17

Du har også doall

leifericf06:06:16

Skjønner! Takk for tålmodigheten og at du gidder å forklare ting på en n00b-vennlig måte 😅

cjohansen06:06:42

Jeg skal til og med by på et ålreit use-case for doall: Du kan bruke den sammen med pmap for å parallellisere arbeidet:

(->> (get-all-repo-data)
     (tree-seq coll? identity)
     (keep :sshUrl)
     (pmap clone-repo)
     doall)

😮 2
leifericf06:06:33

Det er fett, da! Går det brått mye raskere.

leifericf06:06:56

Nå spørs det hvor happy Azure DevOps CLI er med at jeg kjører masse clones samtidig, men det finner vi fort ut av 😅

slipset06:06:58

pmap var en av tingene som solgte meg på Clojure. Å skrive parallell kode har liksom alltid vært skummelt. I Clojure var det bare å legge til en p og det var det. Fordelen og ulempen med pmap er at den er veldig lite tune-bar, faktisk ikke i det hele tatt.

cjohansen06:06:12

Den gjør ett eller annet relativt til antall kjerner eller noe sånt?

cjohansen06:06:26

Jeg har primært brukt den fra REPL-et

msolli08:06:31

@leif.eric.fredheim Siden du er i utforskermodus, anbefaler jeg https://clojure.org/api/cheatsheet. Funksjonene dere er fint gruppert under forskjellige overskrifter. doseq med venner finner du under SequencesUsing a SeqForce evalutation.

👀 2
leifericf08:06:55

@U06BEJGKD: Den er fin! Mange takk!

leifericf10:06:24

@christian767 Ref. eksempelet i https://clojurians.slack.com/archives/C061XGG1W/p1686552282934989?thread_ts=1686322032.104469&amp;cid=C061XGG1W med pmap + doall. Hvis jeg forstår rett, trenger man ikke å bruke doall her, fordi pmap i seg selv er "eager." Stemmer det?

cjohansen10:06:27

Jeg som ikke vet hva jeg prater om 😅

msolli10:06:38

> “Like map, except f is applied in parallel. Semi-lazy in that the > parallel computation stays ahead of the consumption, but doesn’t > realize the entire result unless required. Only useful for > computationally intensive functions where the time of f dominates > the coordination overhead.”

😮 2
msolli10:06:40

(Docstringen til pmap)

msolli10:06:09

Så jo, du må bruke doall.

leifericf10:06:55

Så rart! Når jeg fjernet doall fra koden min så ser jeg ingen forskjell når jeg bruker pmap. Det ser ut til at hele collection blir "realisert" uansett.

magnars10:06:08

du gjør det kanskje i replet?

leifericf10:06:09

Men kanskje det er fordi lista ikke er stor nok til å bli delt opp?

cjohansen10:06:10

Prøv med en større collection

magnars10:06:13

replet realiserer den for deg

💡 2
magnars10:06:20

den chunker også med 32 items av gangen

leifericf10:06:37

Maddafakkas. Det forklarer saken. REPL "lurte" meg 😂

magnars10:06:56

har blitt grundig lurt av den sjæl, for å si det sånn 🙈 😅

😂 2
cjohansen10:06:16

Har fortsatt mareritt fra en sånn seanse på SPID i 2013 @magnars 😂

😂 2
magnars10:06:18

"HVORFOR GJØR KODEN MIN INGENTING" ... (for ...

leifericf10:06:10

Det er godt å høre at selv dere erfarne folk snubler i de samme trådene noen ganger da! Gir meg litt peace of mind 😂

magnars10:06:15

med for kan man løse det ved å bytte ut med doseq som har samme signatur (men returnerer nil)

leifericf10:06:39

Vurderer om jeg skal bruke reducers istedenfor.

cjohansen10:06:54

Jeg foretrekker doseq fordi den også veldig tydelig kommuniserer at koden gjør side-effekter

👍 2
leifericf10:06:11

doseq istedenfor reducers?

magnars10:06:29

doseq hvis man gjør imperative shit, siden den ikke kan returnere noe

👍 4
magnars11:06:08

det blir litt som å bruke when istedenfor if hvis man har bare én klausul. Den reduserte friheten kommuniserer tydeligere.

leifericf11:06:00

Affirmative. Min nye tommelfingerregel blir heretter "prefer doseq for imperative shit"

slipset11:06:17

det som er litt drit er hvis du ønsker å si noe om hvordan det imperative gikk.

slipset11:06:23

Da kan du gjerne bruke reduce

leifericf11:06:41

Typ hvis man ønsker å "fange" resultatet på et vis?

slipset11:06:09

Jepp. Nå kommer sikkert @magnars og sier at jeg gjør noe feil.

😂 2
leifericf11:06:22

doseq med reduce inni for good measure? 😛

magnars11:06:41

@slipset skal ikke være så bastant, men har ikke selv hatt det behovet. Som oftest er det data på vei inn som kan inspiseres. Best å gjøre så lite som mulig inni selve "do shit"-loopen.

leifericf11:06:07

Men en ting jeg merker nå… Jeg har denne versjonen istedenfor, som funker fett!

(defn clone-all-repos
  [repos]
  (doseq [url (extract-key :sshUrl repos)]
    (clone-repo url)))
"Problemet" er at den er mye treigere, antagelig fordi doseq ikke er parallellisert. Og den kjører mange git clone commands, som kan ta litt tid hver seg. Hva er "best practice" for "do-shit-loops" med side-effects hvis det hele skal parallelliseres? Er det da en drar fold ut av skuffen?

magnars11:06:36

Hvis du ikke trenger kontroll over timingen så er kanskje pmap + doall like greit. Ellers har du mer styring hvis du lager din egen lille maskin med core.async.

😅 2
leifericf11:06:47

Ja, i dette tilfellet trenger jeg ikke kontroll over timingen. Jeg vil bare clone alle repos raskest mulig egentlig.

leifericf11:06:49

Men! Jeg har faktisk også opplevd en merkelig runtime error et par ganger med pmap + doall. Får randomly en eller annen Java threading error, så blir den borte når jeg kjører på nytt.

msolli11:06:48

Det er kanskje det enkleste. Da får du like mange parallelle nedlastinger som pmap bruker internt, hva enn det er. Hvis du vil ha kontroll over graden av parallellitet kan du bruke core.async, som @magnars nevnte (`pipeline-blocking` f.eks.), eller du kan sette opp en egen Executor (thread pool) som du konfigurere akkurat som du vil (https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newFixedThreadPool-int-). For ditt formål her og nå er det kanskje å gå alt for langt, men nyttig å vite om.

💡 2
leifericf11:06:10

Nice, takk for tipset! Ja, jeg tror 32 parallelle nedlastninger (eller hva det nå blir) er godt nok her. I mitt tilfelle er det færre enn 1000 repos, så det går ganske radig med pmap. Typ under 10 sekunder.

leifericf13:06:16

Jeg fikk litt problemer med at doseq inni en funksjon gjorde at funksjonen heller ikke returnerte korrekt verdi, og jeg fikk "unused value" warnings av clj-kondo. Så jeg forsøkte meg på reduce istedenfor. Gir https://github.com/leifericf/leifs-utils/commit/b63d3458534afa6dbc694a986f4623aebe6ad4c2 mening? :thinking_face: Når man må "fange resultatet" for å returnere det fra funksjonen, slik @slipset var inne på tidligere. Det funker, men jeg synes det er litt vanskeligere å lese.

slipset13:06:52

Trenger ikke bruke reduce over alt. bare på slutten.

slipset13:06:39

Så lenge du har noe på slutten som realiserer alt som er lazy så er Bob onkelen din.

slipset13:06:07

Å reimplementere map med reduce bare for å få eagerness er bleh.

👍 2
leifericf13:06:45

Hvis jeg ville bruke doseq — Hva kan jeg bruke på slutten for å realisere alt? Sånn at funksjonene returnerer resultatet av doseq. Hvis det gir mening.

slipset13:06:02

Gir ikke mening.

😅 2
leifericf13:06:39

Jeg skal prøve å lage et konkret eksempel for å illustrere det jeg prøver å si.

slipset13:06:23

(->> (file/glob search-path "**.git" {:hidden true})
       (map file/parent)
       (map str)))
Er helt fint. ikke noe grunn til å bruke reduce/doseq her, så lenge det er noe annet lenger ut som bruker reduce/doseq for å realisere det som blir produsert her.

💡 2
slipset13:06:36

Og bare for å gi deg noe å tygge på: (reduce #(conj %1 (str %2)) [] xs)

slipset13:06:43

kan du like gjerne skrive som

slipset13:06:51

(->> xs (map str) (reduce conj []))

2
slipset14:06:44

Eller, hvis du ikke vil briefe med at du kan reduce:

(mapv str xs)
Men vi ble vel enige tidligere om at å bruke mapv bare fordi du vil realisere en lazyseq er litt styggedom

👍 2
leifericf14:06:58

Aha, jeg tenkte at siden file/glob gjorde "side-effecting shit," så burde man ikke bruke map inne i den funksjonen heller 😅

slipset14:06:47

Det kan du fint gjøre, bare husk at exceptions da kommer når du realiserer sekvensen, dvs på et annet sted enn der du ville forvente.

💡 2
leifericf14:06:33

Oh, right! Mye å lære seg merker jeg! I dag var ikke en spesielt produktiv, men lærerik dag 😛

leifericf14:06:09

Det neste spørsmålet som poppet opp i hodet mitt da er: Hvor langt "ned" bør man realisere sekvenser? Intuitivt tenker jeg at de bør realiseres så langt "opp" som mulig der de brukes og ikke i selve implementasjonen av funksjonen som produserer dem :thinking_face: Lukter at det kanskje finnes en ny tommelfingerregel til @U0MKRS1FX sin neste bok her et sted.

leifericf14:06:39

Hvis jeg forstår deg rett: Funksjonen foo returnerer en sekvens via map. Funksjonen bar kaller foo og vil printe ut alt i sekvensen fra foo. Da er det bar sin jobb å realisere sekvensen. Korrekt?

slipset14:06:45

Jepp

🎉 2
leifericf14:06:16

Forstått! Takk igjen for tålmodigheten og forklaringen 🙇

slipset14:06:43

Tenk deg:

(defn foo [x]
  (range (+ 10 x)))
Denne returnerer en uendelig lazy seq av tall
(defn bar [n xs] (println (take n xs)))
Da kan du

slipset14:06:59

(bar 10 (foo 11))

leifericf14:06:55

Wow, kult! Dette er ganske mind-blowing stuff 🤯

leifericf14:06:41

Så enkelt, men så vanskelig å forstå også av en eller annen grunn. Jeg må pakke ut noen eksempler forsiktig i REPL i morgen.

augustl14:06:44

lazy er rått. Det muliggjør en god del forenkling og færre linjer kode https://augustl.com/blog/2019/amazing_lazy_data_structures/

👀 2
leifericf14:06:39

Clojure får meg til å føle meg smart og dum samtidig. I love it!

magnars15:06:38

Ettersom ingen har svart på spørsmålet ditt enda, @leif.eric.fredheim - det er doall du kaller på en lazy seq for å realisere den.

💡 4
leifericf17:06:27

Takk, @magnars! Det forklarer hvorfor doall etter pmap helt i starten av tråden 😅

cjohansen17:06:01

nitpick, men kode ved siden av argument-lista ser rart ut for meg. jeg ville gjort sånn:

(defn clone-all-repos
  "Clone all Git repos from Azure DevOps."
  []
  (->> (get-all-repo-data)
       (tree-seq coll? identity)
       (keep :sshUrl)
       (run! clone-repo)))

leifericf06:06:07

Nitpick er bra! Man lærer av sånt. Jeg går faktisk mye frem og tilbake på dette og er ikke sikker på hva jeg foretrekker, spesielt når funksjoner har multiple arities, f.eks.:

(defn clone-repo
  "Clone a Git repo to a destination path."
  ([repo-url] (clone-repo repo-url (str (file/home) (get-secret :repo-root-dir))))
  ([repo-url dest-path] (let [path dest-path]
                          (if-not (file/exists? path) (file/create-dir path) nil)
                          (process/sh {:dir path} "git clone" repo-url))))
Jeg synes det er lettere å lese det når body kommer rett etter argument-lista den "tilhører" når det er flere implementasjoner, spesielt hvis noen arities har korte bodies. Men det kan bare være fordi jeg er n00b og ikke har vendt meg til å lese Clojure-kode helt enda.

cjohansen09:06:28

Dette blir veldig subjektivt og synsete, men jeg er generelt var for ting som skyver koden ut til høyre. Det gir dårlig utnyttelse av plass og kan gjøre det vanskeligere å splitte skjermen. Jeg bruker svært sjelden multi-arity, men om jeg gjør det så bruker jeg heller en blank linje for å skille arityene. Dessuten er de pakket i sin egen parantes og kan pared-navigeres.

💡 2
leifericf09:06:55

Flere gode poeng der som jeg ikke hadde tenkt på!

augustl07:06:33

jeg løser også dette med “newline galore”. Jeg er glad i whitespace 🙂

(defn clone-repo
  "Clone a Git repo to a destination path."
  ([repo-url] 
   (clone-repo repo-url (str (file/home) (get-secret :repo-root-dir))))
  
  ([repo-url dest-path]
   (when-not (file/exists? path) (file/create-dir path))
   (process/sh {:dir path} "git clone" repo-url)))

👍 2
augustl07:06:05

(og alle steder jeg kan bruke when og when- i stedet for if og if- blir jeg glad og fornøyd)

cjohansen17:06:41

Lykke til med foredrag @teodorlu! Er det i morgen?

❤️ 2
teodorlu17:06:01

Sitter med siste finpuss nå.

cjohansen17:06:54

Blir sikkert bra 🙂

teodorlu17:06:58

Satser på det! 😊 Tok en prøvekjøring i lokalet med en fyr som var villig til å høre på. Veldig glad jeg gjorde det. Fikk plukket ut noe rusk. Har mest slides, men skal vise bittelitt fra Emacs også. Hvis vi ikke hadde kjørt testen, hadde jeg hatt aaaaalt for liten font i Emacs. Størrelse 20 ("huge") holdt ikke. Nå har jeg:

(defun teod/font-megahuge ()
  (interactive)
  (setq doom-font (font-spec :family "monospace" :size 40))
  (doom/reload-font))

👍 2
cjohansen17:06:11

Jeg pleier å kjøre zoom-frm-in til folk bakerst er fornøyd

👍 2
cjohansen17:06:15

men hurra for Emacs ❤️

emacs-spin 2
leifericf06:06:07

Nitpick er bra! Man lærer av sånt. Jeg går faktisk mye frem og tilbake på dette og er ikke sikker på hva jeg foretrekker, spesielt når funksjoner har multiple arities, f.eks.:

(defn clone-repo
  "Clone a Git repo to a destination path."
  ([repo-url] (clone-repo repo-url (str (file/home) (get-secret :repo-root-dir))))
  ([repo-url dest-path] (let [path dest-path]
                          (if-not (file/exists? path) (file/create-dir path) nil)
                          (process/sh {:dir path} "git clone" repo-url))))
Jeg synes det er lettere å lese det når body kommer rett etter argument-lista den "tilhører" når det er flere implementasjoner, spesielt hvis noen arities har korte bodies. Men det kan bare være fordi jeg er n00b og ikke har vendt meg til å lese Clojure-kode helt enda.

leifericf06:06:43

Jøss! Det kan se sånn ut, ja, men jeg forstår ikke hvorfor 😅 Hvorfor prosesserer ikke map hele lista med alle repo URL-er? :thinking_face:

leifericf06:06:36

run! var relativt enkelt å bruke, fordi jeg kunne simpelthen bytte ut map med run!. Men hvordan bruker jeg doseq i denne thread-last makroen? I og med at doseq trenger en vector med bindings. Må jeg pakke inn hele thread-last makroen i doseq på et vis, eller skrive om hele body uten å bruke thread-last makroen? :thinking_face:

(defn clone-all-repos
  []
  (->> (get-all-repo-data)
       (tree-seq coll? identity)
       (keep :sshUrl)
       (run! clone-repo)))