Fork me on GitHub
#clojure-norway
<
2023-06-20
>
teodorlu07:06:01

Jeg har prøvd meg litt på å skrive makroer, men jeg bruker i dag ingen av makroene jeg har skrevet. Det meste kunne løses med funksjoner, data og makroer andre har skrevet. Når var forrige gang du brukte en makro du selv har skrevet?

magnars07:06:11

(defmacro forcat
  "`forcat` is to `for` like `mapcat` is to `map`."
  [& body]
  `(mapcat identity (for ~@body)))

😁 2
👍 2
teodorlu07:06:57

jaaa, brukeren returnerer en liste av lister, så "flates" den ned til én liste?

magnars08:06:50

stemmer 👍

augustl08:06:06

særlig kjekt når man driver og komponerer f.eks funksjoner som returnerer ting som skal sendes inn til datomic/datascript transactions 😄

👍 2
augustl08:06:59

er egentlig generelt mange settinger hvor det er digg å kunne ha en samling funksjoner som returnerer lister, så du kan ha funksjoner som returnerer 0, 1 eller mange av noe uten å endre for mye på ting, og så mapcatte det sammen til slutt etterpå

👍 2
augustl08:06:30

FC/IS 🥳

❤️ 2
leifericf10:06:41

Gjør ikke clojure.core/flatten noe lignende? :thinking_face:

augustl10:06:02

flatten går all the way (rekursiv)

leifericf10:06:09

Flatten går all the way ( ͡° ͜ʖ ͡°)

😎 2
magnars10:06:04

Du vil nesten aldri ha flatten .

👂 2
leifericf10:06:08

Jeg fikk en "dobbel liste" i min funksjon sh->out når den ble kalt fra sh-out->json:

(defn sh->out
  [opts & args]
  (-> (apply process/sh opts (flatten args))
      :out))

(defn sh-out->json
  [opts & args]
  (-> (sh->out opts args)
      (json/parse-string true)))
Klarte ikke å finne ut hvorfor, så jeg var lat og brukte flatten 😅 Sikkert ikke lurt.

augustl10:06:21

nei, den er vel så godt som et anti-pattern og kan fort by på overraskelser

augustl10:06:36

nemlig, ofte så vet du hvor mange nivåer du vil “ta bort”, og kan bruke f.eks mapcat i stedet for

augustl10:06:21

dobbel liste her kommer vel forøvrig fra & args “to ganger. Ved første kall er args en liste, så sender du den inn til en annen som er & args og da får en liste med det første argumentet, som er en liste 😄

😅 2
leifericf10:06:42

#newbieproblems

augustl10:06:20

((fn [& args] (prn args)) 1 2 3) => (1 2 3)

augustl10:06:18

((fn [& args1] ((fn [& args2] args2) args1)) 1 2 3) => ((1 2 3))

leifericf10:06:24

Men hvorfor blir lista pakken inn i en annen liste når den allerede var en liste? 😅

augustl10:06:41

& args er ikke “smart”, den får inn et agument som er en liste, og vil ikke automatisk explode den basert på type

💡 2
augustl10:06:10

kan eventuelt bruke apply - ((fn [& args1] (apply (fn [& args2] args2) args1)) 1 2 3) => (1 2 3)

💡 2
augustl10:06:20

eller la være å bruke & args da, hvis du faktisk ikke tenker å kalle den variadic

augustl10:06:17

(altså sende inn N argumenter, i stedet for å sende inn en liste som ett argument)

leifericf10:06:32

sh-out->json brukes slik, så tror den må være variadic:

(defn get-devops-project-repo-data
  [project-id]
  (sh-out->json "az" "repos" "list"
                "--project" project-id))

(defn get-github-repo-data
  []
  (sh-out->json "gh" "repo" "list" (:github/org-name settings)
                "--language" (:github/repo-language-filter settings)
                "--source"
                "--no-archived"
                "--limit" "1000"
                "--json" "sshUrl"))

augustl10:06:41

stemmer 👍

augustl10:06:01

men sh->out trenger kanskje ikke å være det?

augustl10:06:22

det er ihvertfall der det blir “feil” og den kommer dobbelt

leifericf10:06:55

sh->out brukes slik nå:

defn clone-repo
  [repo-url]
  (let [path (get-repo-root-path)]
    (if-not (file/exists? path) (file/create-dir path) nil)
    (sh->out {:dir path} "git" "clone" repo-url)))

(defn run-git-command
  ([command]
   (run-git-command (find-repo-paths (get-repo-root-path)) command))

  ([root-path command]
   (->> root-path
        (pmap #(sh->out {:dir %} "git" command))
        (doall))))

augustl10:06:04

ah den brukes også direkte sånn ja

leifericf10:06:17

Ja, men det er kanskje dårlig design når jeg tenker meg om…

magnars10:06:24

(defn sh-out->json
  [opts & args]
  (-> (apply sh->out opts args)
      (json/parse-string true)))

🚀 2
clojure-spin 2
leifericf10:06:26

Det er noen ganger jeg må ha JSON, og andre ganger ikke.

leifericf10:06:25

Lurte på om jeg kanskje kunne brukt multiple arities eller multimethods for å unngå to separate funksjoner.

leifericf10:06:47

Eller kanskje partial på et vis.

augustl10:06:53

synes egentlig det virker greit å ha to funksjoner sånn, en for et “rent” kall, og en som kaller den rene og gjør noko attått

👍 2
leifericf10:06:11

Men fikla med det lenge og slo meg til ro med to funksjoner for å komme videre med det jeg prøvde å oppnå 😅

augustl10:06:14

i JS har man syntaks for sånt, man kan explode med . apply gjør samme nytta i clj 😄

augustl10:06:47

men, ofte synes jeg det er greit å rett og slett bare sende inn ei liste, nettopp pga ting som dette, og at noen ganger kan det være kjekt å kunne bygge opp en liste programmatisk og sende det inn til funksjonen uten å måtte kalle apply på den først osv

👍 2
2
augustl10:06:45

altså at man bare kaller den med (sh-out->json "gh" ["repo" "list" x "--source"]) etc. En smakssak 👍

leifericf10:06:20

Jeg tror ikke det vil fungere pga. Babashka sin interne "tokenizer" gjør noe fancy shit for å konvertere lista med strenger til shell script stuff. Men jeg skal teste litt!

leifericf10:06:34

Fikk litt hjelp i #CLX41ASCS tidligere og selveste borkdude sa jeg bare burde sende inn en liste med strenger til babashka/sh.

augustl10:06:37

bør vel ikke ha noe å si om du sender inn en “args” som en liste sånn, eller varargs med “& args” (om det ga mening)

augustl10:06:56

selve kallet til sh må kanskje kalles med varargs ja, men du kan jo velge å gjøre det annerledes i dine egne funksjoner

2
leifericf10:06:28

Aha, sånn, ja. Tror jeg skjønner hva du mener!

augustl10:06:49

du står fritt til hvordan du vil gjøre det med dine egne wrapper-funksjoner lizm!

👍 2
magnars07:06:03

Jeg bruker også denne en del i main-metoden av appen min:

(defmacro with-timing [name exp]
  `(do
     (print "Time for" ~name "... ")
     (flush)
     (let [start# (System/currentTimeMillis)
           res# ~exp]
       (println (- (System/currentTimeMillis) start#) "ms")
       res#)))

👍 2
cjohansen07:06:01

I min erfaring blir makroer blir veldig oversolgt når man først setter seg inn i Lisp. Det er sjelden kost. Nyttige når de er løsningen, men bør brukes sparsomt.

magnars07:06:07

men forøvrig så er det veldig sjelden bruk for å lage sine egne makroer

👍 2
cjohansen07:06:22

Vi har noen makroer i testene våre, with-test-system feks

teodorlu08:06:27

Hva er det den gjør?

(with-test-system [sys (test-system)]
  ,,,)
? Antar det ikke er akkurat det den gjør, for da kunne man vel
(let [sys (test-system)]
  ,,,)

cjohansen08:06:33

Den gjør det, pluss at den stenger ned ting på slutten.

teodorlu08:06:32

Aaah, skjønner 👍

augustl08:06:38

makroer putter jeg i seksjonen for “språk-funksjon som er nyttig for de som lager libraries” 😄 Føler alle språk er litt seksjonerte der. Ikke så mange som bruker contracts i Kotlin i CRUD-en sin liksom

👍 2
augustl08:06:58

men uten makroer hadde vi jo ikke hatt core.async og defscene i portifolio og andre nyttige greier!

👍 2
cjohansen08:06:27

defscene er et nokså tynt lag med convenience - sånn makroer burde være 😄

💯 2
👍 4
augustl08:06:16

finner 79 kall til defmacro i clojure.core - så det er ikke så mye brukt selv der

👍 2
augustl08:06:01

and og or drar jo nytte av å være macros, er vel sånn den evaluerer én og én ting som sendes inn etter tur, i stedet for å evaluere alle argumentene først og så gå igjennom de

👍 2
augustl08:06:26

og våre gode venner -> og ->> må nevnes

👍 2
teodorlu09:06:04

Jeg tenkte på makroer fordi det kom et https://clojurians.slack.com/archives/C03S1KBA2/p1687196467018599: > Is there a way to do this? >

(def [a b c] [1 2 3])
> I know I could do >
(def a 1)
> (def b 2)
> (def c 3)
> or >
(def temp-coll [1 2 3])
> (def a (nth temp-col 0))
> (def b (nth temp-col 1))
> (def c (nth temp-col 2))
> or something clever with "binding" Og jeg reagerer i både med "oi, her kan vi bruke makroer!!!" og "er dette virkelig noe du ønsker å gjøre, det virker litt rart?". Så jeg ender opp med å skrive en greie:
(defn defmany* [syms bodies]
  `(do
     ~@(map (fn [sym body]
             `(def ~sym ~body))
           syms bodies)))

(defmacro defmany [syms bodies]
  (defmany* syms bodies))

(macroexpand-1
 '(defmany [a b c] [1 2 3]))
;; => (do (def a 1) (def b 2) (def c 3))
... men jeg deler den ikke fordi dette ikke er noe jeg ville sjekket inn i min egen kode. Gøy å lage ting, men dumt å lage ting som bare lager trøbbel.

Ivar Refsdal18:06:06

Konteksten for spørsmålet var REPL-poking, så vidt eg forstod... Så det bør ikkje sjekkast inn i/på classpath. Kor praktisk det er med ein slik sak, tja, ikkje veit eg.. Eg snappa opp følgande på #clojure ei stund tilbake:

(ns user)

(ns clojure.core)

(defmacro def-let
  "like let, but binds the expressions globally."
  [bindings & more]
  (let [let-expr (macroexpand `(let ~bindings))
        names-values (partition 2 (second let-expr))
        defs   (map #(cons 'def %) names-values)]
    (concat (list 'do) defs more)))

(defn pp [x]
  ((resolve 'clojure.pprint/pprint) x)
  x)
det ligger i dev/user.clj og vert automatisk lasta når ein starter opp eit REPL (gitt det er på classpath). Då får ein def-let og pp (automatisk) tilgjengeleg i alle namespace. Einig i at makroer bør brukast sparsomleg, og glad over at andre her ser ut til å meina det same. https://github.com/ptaoussanis/tufte har defnp, som eg synest er heilt okki iallefall. clj-kondo har støtte for linting av ting som "liknar på" defn, let, osb. Eg lagde ein in-house makro inspirert av det, defnl, der l står for logged, og eksponerte feilstatistikken ut i ein helsesjekk. Og det igjen gjorde at eg såg ein deploy som hadde gått grønt (med nytt sertifikat), men som no feila. Eg burde vel sjekka loggen, men det er langt raskare (for meg iallefall), å sjekka helseendpointet. Dette igjen (puh) gjorde at eg byrja å skrive eit bibliotek for det: https://github.com/ivarref/defnlogged... Og så googla eg alternativer litt seint, og såg at det meste allereie finst 🙃. Usikker på om det som finst støtter nøyaktig det same, men anyways... Oh well. Så kanskje eg berre kaster/dropper det biblioteket: for custom-made makroer på classpath i prod er ein uting (?) </wall-of-text>

👍 2
teodorlu20:06:00

> konteksten var REPL-poking > Jaaa, sant. Da gir det jo mer mening, jeg ønsker å sjekke hva mellomverdiene var.

teodorlu20:06:17

Tufte var spennende! 💯

🚅 2
🚀 2
augustl10:06:09

ser ikke helt problemet med å skrive tre defs der nei 🙂

4
leifericf10:06:17

En ting jeg har stusset litt på. Nå gjør jeg dette default varier på parametere:

(defn foo
  ([]
   (foo 42))
  
  ([n]
   baz (n)))
Finnes det andre måter å gjøre det på? Noen språk lar en gjøre det direkte i signaturen til funksjonen, så det blir noe mindre kode. F.eks. i C#:
public void Foo(int bar = 42)
{
    Baz(bar);
}
Eller Python:
def foo(bar = 42):
    baz(bar)

magnars10:06:32

Ja, du kan bruke :keys -destructuring med default parametere, men foreslår at du får det meste styrer unna

👂 2
magnars10:06:04

det er bedre om funksjonene dine ikke har så mange forskjellige måter å kalles

augustl10:06:09

fancy destructuring er en fin måte å skrive kode som er vanskelig å lese ja 😅 Ofte foretrekker jeg å bare fiske ut defaults i en let-block eller noe sånt

augustl10:06:37

😎

👀 2
augustl10:06:28

litt usikker på hva :or betyr. Vil tro den bare binder om nøkkelen ikke eksisterer i mappet? Eller vil den også binde om verdien er satt til nil i mappet? 😅

mariuene10:06:29

Det stemmer. Har gått på den feilen en gang 😅

(defn a [{:keys [foo] :or {foo false}}]
  foo)
=> #'user/a
(a {:foo nil})
=> nil
(a {})
=> false

💡 2
😅 2
leifericf12:06:38

Jeg er ganske fornøyd med å ha fått dette til å funke i dag:

(defn find-in-file [file-path pattern]
  (with-open [reader (io/reader file-path)]
    (->> (line-seq reader)
         (keep-indexed #(when (str/includes? %2 pattern)
                          {:path file-path
                           :line (inc %1)
                           :column (inc (.indexOf (str %2) pattern))}))
         (into []))))
Den søker etter et pattern i en fil, og returnerer maps med path, linje og kolonne for lokasjon der pattern matchet. For eksempel, brukes slik for å finne alle mikrotjenester som bruker en gitt versjon av .NET:
(->> (find-files (get-repo-root-path) ["csproj"])
     (map str)
     (pmap #(find-in-file % "netcoreapp3.1"))
     (remove empty?))
Returnerer eksempelvis:
[{:file-path
   "/Users/leif/Code/foo.csproj",
   :line 4,
   :column 22}]
 [{:file-path
   "/Users/leif/Code/bar.csproj",
   :line 4,
   :column 22}]
 [{:file-path
   "/Users/leif/Code/baz.csproj",
   :line 4,
   :column 22}]
 ...)
De fleste .csproj filer har lik struktur. Derfor er :line og :column det samme. Dette var den første versjonen av funksjonen (kopiert rått fra ChatGPT vel å merke), som også fungerte, men var blodstøgg:
(defn find-in-file [file-path pattern]
  (with-open [reader (io/reader file-path)]
    (let [lines (line-seq reader)]
      (loop [line-num 1
             remaining-lines lines
             results []]
        (if-let [line (first remaining-lines)]
          (let [index (.indexOf line pattern)]
            (if (not= -1 index)
              (recur (inc line-num) (rest remaining-lines)
                     (conj results {:file-path file-path
                                    :line line-num
                                    :column (inc index)}))
              (recur (inc line-num) (rest remaining-lines) results)))
          results)))))

augustl12:06:29

man klarer stort sett å komme seg unna loop ja! Utrolig hva som finnes i core fra før

leifericf13:06:25

Yeah! Og dette kjører typ 10x raskere enn Bash scriptet jeg hadde, antagelig pga. pmap

augustl13:06:15

synes det gir veldig mening å lage toolinga si i noe annet enn shellscript!

👍 2
leifericf13:06:52

Indeed! Babashka er helt rått. Det har sikkert tatt meg 10x lengre tid å oppnå sluttresultatet jeg prøver å oppnå (😳), men nå lærer jeg litt Clojure samtidig og får noen utils som er enklere å gjenbruke for lignende oppgaver i fremtiden 😁

leifericf13:06:01

Pluss at kollegaer går forbi, ser på skjermen min og spør "hva i all verden er det der?!" 😂 Fin anledning til å vise litt!

slipset20:06:44

det som er litt festlig der er at du redder deg inn med en (into []) på slutten

😅 2
augustl07:06:08

ja, en doall ville vel gjort samme nytta

augustl07:06:14

med mindre du trenger at det er en vector av en eller annen grunn

slipset07:06:38

Så for å ta tak i denne problematikken, fordi den kommer til å bite deg. I gamle dager, var Hibernate kult. Hibernate hadde lazy loading av relasjoner, som også var kult, men som også var tidenes foot gun. Et av problemene var at du måtte ha en åpen connection til databasen når man realiserte lazyness’en. Enter open-session-in-View eller deromkring. Grunnen til at jeg tar det opp er at vi treffer det samme problemet i Clojure med lazy seqs. I koden over bruker du with-open-et-eller-annet. Når du er ferdig med den (leksikalske) bindingen er filen din lukket. Hvis du d prøver å realisere seq’en din, går ting normalt til helvete. En annen artig felle der er hvis du bruker with-redefs og returnerer en lazy seq, så vil redef’en ikke være synlig når den realiseres. Har tapt litt tid på akkurat det.

👍 2
augustl07:06:37

nesten så core kunne hatt with-open2 og with-redefs2 som sjekket om du returnerte en LazySeq og automatisk realiserte den for deg som en del av “pakka”

augustl07:06:14

samme med with-connection og generelt alskens with-X der ute

augustl07:06:35

(eventuelt bare bruke Datomic hvor database-viewet ditt også er lazy 😎)

leifericf07:06:16

Jeg setter pris på innspillene deres! Det er den beste måten å lære på når folk peker ut dumme ting jeg gjør.

🥳 2
augustl07:06:06

..og dumme ting Clojure gjør :S Ble sittende å tenke på hvordan Pinnacle Of Dev Experience next.js 100% hadde hatt en linter eller runtime-sjekk eller bare automatisk realiserte lazy-seqs i with-open osv. Clojure har liksom bestemt seg for å ikke være sånn “lett” å ha med å gjøre, men heller la foot gunsa ligge

leifericf07:06:28

Er dette litt bedre? :thinking_face:

(defn find-in-file [file-path pattern]
  (->> (io/file file-path)
       (io/reader)
       (line-seq)
       (map-indexed #(when (str/includes? %2 pattern)
                       {:path file-path
                        :line (inc %1)
                        :column (inc (.indexOf %2 pattern))}))
       (filter identity)))

leifericf08:06:07

Kanskje enda litt bedre 🙂

(defn find-in-file [file-path pattern]
  (->> file-path
       (file/read-all-lines)
       (map-indexed #(when (str/includes? %2 pattern)
                       {:directory (str (file/parent file-path))
                        :filename (file/file-name file-path)
                        :pattern pattern
                        :line (inc %1)
                        :column (inc (.indexOf %2 pattern))}))
       (filter identity)))

augustl09:06:00

hva er file/read-all-lines ?

leifericf10:06:02

Det er en Babashka-greie, babashka.fs :as file

augustl10:06:07

en eventuell fordel med io/reader og line-seq er at den såvidt jeg vet er lazy

👍 2
augustl10:06:23

så om du leser ei svær fil så trenger du ikke laste inn hele fila i RAM først, den kan streames inn mens den prosesseres

💡 2
augustl10:06:29

hilsen August “Kongen Av Premature Optimization” Lilleaas 😄

😂 2
Ivar Refsdal18:06:06
replied to a thread:Jeg tenkte på makroer fordi det kom et https://clojurians.slack.com/archives/C03S1KBA2/p1687196467018599: > Is there a way to do this? > (def [a b c] [1 2 3]) > I know I could do > (def a 1) &gt; (def b 2) &gt; (def c 3) > or > (def temp-coll [1 2 3]) &gt; (def a (nth temp-col 0)) &gt; (def b (nth temp-col 1)) &gt; (def c (nth temp-col 2)) > or something clever with "binding" Og jeg reagerer i både med "oi, her kan vi bruke makroer!!!" og "er dette virkelig noe du ønsker å gjøre, det virker litt rart?". Så jeg ender opp med å skrive en greie: (defn defmany* [syms bodies] `(do ~@(map (fn [sym body] `(def ~sym ~body)) syms bodies))) (defmacro defmany [syms bodies] (defmany* syms bodies)) (macroexpand-1 '(defmany [a b c] [1 2 3])) ;; =&gt; (do (def a 1) (def b 2) (def c 3)) ... men jeg deler den ikke fordi dette ikke er noe jeg ville sjekket inn i min egen kode. Gøy å lage ting, men dumt å lage ting som bare lager trøbbel.

Konteksten for spørsmålet var REPL-poking, så vidt eg forstod... Så det bør ikkje sjekkast inn i/på classpath. Kor praktisk det er med ein slik sak, tja, ikkje veit eg.. Eg snappa opp følgande på #clojure ei stund tilbake:

(ns user)

(ns clojure.core)

(defmacro def-let
  "like let, but binds the expressions globally."
  [bindings & more]
  (let [let-expr (macroexpand `(let ~bindings))
        names-values (partition 2 (second let-expr))
        defs   (map #(cons 'def %) names-values)]
    (concat (list 'do) defs more)))

(defn pp [x]
  ((resolve 'clojure.pprint/pprint) x)
  x)
det ligger i dev/user.clj og vert automatisk lasta når ein starter opp eit REPL (gitt det er på classpath). Då får ein def-let og pp (automatisk) tilgjengeleg i alle namespace. Einig i at makroer bør brukast sparsomleg, og glad over at andre her ser ut til å meina det same. https://github.com/ptaoussanis/tufte har defnp, som eg synest er heilt okki iallefall. clj-kondo har støtte for linting av ting som "liknar på" defn, let, osb. Eg lagde ein in-house makro inspirert av det, defnl, der l står for logged, og eksponerte feilstatistikken ut i ein helsesjekk. Og det igjen gjorde at eg såg ein deploy som hadde gått grønt (med nytt sertifikat), men som no feila. Eg burde vel sjekka loggen, men det er langt raskare (for meg iallefall), å sjekka helseendpointet. Dette igjen (puh) gjorde at eg byrja å skrive eit bibliotek for det: https://github.com/ivarref/defnlogged... Og så googla eg alternativer litt seint, og såg at det meste allereie finst 🙃. Usikker på om det som finst støtter nøyaktig det same, men anyways... Oh well. Så kanskje eg berre kaster/dropper det biblioteket: for custom-made makroer på classpath i prod er ein uting (?) </wall-of-text>

👍 2