Fork me on GitHub
#clojure-norway
<
2024-02-09
>
pez08:02:55

Morgen!

👋 3
msolli08:02:18

God morgen!

pez08:02:51

Ska spendera dagen med att flytta över en reagent+re-frame app till dumdom och tar gärna emot tips och trix för det. Just nu undrar jag om någon har en clj-kondo config att dela med sig av 😄. Är kanske även dags för en #dumdom-kanal?

cjohansen10:02:34

Artig 🙂 Jeg har dessverre ikke noe clj-kondo config å gi deg. dumdom burde nok shippe sin egen config så du slipper. Kanskje @U07FCNURX sitter på noe?

pez11:02:37

Om det inte finns en config så kan jag ta fram en. 😃 Just nu gick vi runt det för defcomponent genom att göra ett macro som tar argumenten i samma ordning som defn. (Det var så det var gjort i ett annat projekt här.)

magnars11:02:39

Jeg har ikke laget noen dumdom clj-kondo macro-definisjon, dessverre. Du må gjerne dele hvis dere lager en!

👍 1
pez14:02:02

Det här fungerar för att få en knapp att skicka iväg ett event baserat på ett fält i samma formulär:

(defcomponent foo []
  (let [!input-element (atom nil)
        do-something! (fn []
                        (dd/dispatch-event-data nil [[:foo/do-something (.-value @!input-element)]]))]
    [:div
     [:label {:for "do-something-input"}
      "Do something with:"]
     [:input#do-something-input {:ref (fn [e]
                                        (reset! !input-element e))
                                 :on-key-down (fn [e]
                                                (when (= (.-keyCode e) 13)
                                                  (do-something!)))
                                 :type :text}]
     [:button {:on-click (fn [_e] (do-something!))}
      "Do something!"]]))
Men jag undrar om det bara är mina reagent-reflexer som spökar. Finns det ett dumdommare sätt att göra det?

cjohansen14:02:45

Du trenger ikke å bygge opp tilstanden selv - den ligger i input-elementet

cjohansen14:02:44

Så du kan ihvertfall forenkle det til:

(defcomponent foo []
  [:div
   [:label {:for "do-something-input"}
    "Do something with:"]
   [:input#do-something-input
    {:on-key-down (fn [e]
                    (when (= (.-keyCode e) 13)
                      (dd/dispatch-event-data nil [[:foo/do-something (-> e .-target .-value)]])))
     :type :text}]
   [:button {:on-click (fn [_e] (do-something!))}
    "Do something!"]])

cjohansen14:02:39

keyCode 13 er enter? Det kan være du klarer deg med :on-input ?

cjohansen14:02:14

Ah, sorry, jeg så ikke knappe actionen din

cjohansen14:02:17

Desto større grunn til å bruke :on-input Jeg ville brukt on-input til å lagre verdien i feltet, og så hatt en form rundt med on-submit som "gjør noe" med den verdien

cjohansen14:02:59

dumdom er laget for å ikke ha lokal tilstand, så "the dumdom way" er å lagre det i en global store

cjohansen14:02:28

Omtrent sånn ville jeg gjort det:

(dd/set-event-handler!
 (fn [e data]
   (->> data
        (walk/postwalk
         (fn [x]
           (cond
             (= x :event/target.value)
             (some-> e .-target .-value)

             :else x)))
        process-event-data)))

(defcomponent foo []
  [:form {:on-submit [[:foo/process-value]]}
   [:label {:for "do-something-input"}
    "Do something with:"]
   [:input#do-something-input
    {:on-input [[:foo/collect-value :event/target.value]]
     :type :text}]
   [:button {:type "submit"}
    "Do something!"]])

pez15:02:57

Tack! Det ser mycket trevligare ut än vad jag har.

pez15:02:08

Förstår jag rätt om formuläret nu samlar in data i app-state och [[:foo/process-value]]-hanteraren läser det därifrån?

cjohansen15:02:20

Ja, for eksempel 🙂

cjohansen15:02:46

Alternativt kan du sette dataene fra store rett inn i [[:foo/process-value]], men da ville jeg gjort det utenfor komponenten

cjohansen15:02:21

(defcomponent foo [{:keys [submit-actions input-actions]}]
  [:form {:on-submit submit-actions}
   [:label {:for "do-something-input"}
    "Do something with:"]
   [:input#do-something-input
    {:on-input input-actions
     :type :text}]
   [:button {:type "submit"}
    "Do something!"]])

(defn prepare-foo [state]
  {:input-actions [[:action/assoc-in [:foo] :event/target.value]]
   :submit-actions [[:action/process-value (:foo state)]]})

(render (foo (prepare-foo @app-state)) el)

cjohansen15:02:59

Tanken med dette er at foo (komponenten din) kan være en helt generisk UI-komponent, og så mapper du domenet ditt til UI-data i helt vanlige funksjoner.

pez15:02:30

Jag hänger inte helt med. Men låt mig ge lite mer sammanhang. I reagent+re-frame skulle jag ha det så här:

(defn some-view []
  [:<>
   [bar]
   [foo]])
Sedan tar bar och foo ut vad de behöver via subscriptions och dispatchar eventuellt events. Man kan läsa foo i isolering och se vad den konsumerar och vad den gör. Just nu har hag med dumdom:
(defcomponent some-view [data]
  [:div
   [bar data]
   [foo]])
Jag behöver uppenbarligen lära om, men jag är också väldigt förtjust i att mina komponenter är enbart vektorer… Det gör det lätt att manipulera dem med Clojure, t ex.

cjohansen15:02:21

dumdom legger opp til at du sender data gjennom komponent-treet og har all tilstandshåndtering utenfor ui-komponentene

pez15:02:33

Ja, jag tror att jag tycker om det. Det gör komponenterna lättare att testa. Jag försöker förstå hur mycket det påverkar i övrigt för hur jag kan göra saker.

cjohansen15:02:23

hvis du er vant til å ha subscriptions inne i komponentene så blir det nok en ganske annerledes modell 🙂

pez15:02:40

I min fungerande foo ovan har jag inte ens någon data in. Så det kan inte vara subscriptions-reflexen som spökar, tänker jag. 😃

pez15:02:25

En annan sak. När jag använder ett form och on-submit så laddas hela min SPA om. Använder du något trick för att undvika det?

cjohansen15:02:52

ah, du trenger (.-preventDefault e) på submit-eventet

🙏 1
pez16:02:54

Until I have grokked more of this my form component looks like so:

(defcomponent do-something-form []
  (let [store-path [:store :do-something-input]]
    [:form {:on-submit [[:store/assoc-in store-path]]}
     [:label {:for "do-something"}
      "Do something with:"]
     [:input#do-something {:type :text
                           :on-input [[:store/assoc-in store-path]]}]
     [:button {:type :submit}
      "Do something!"]]))
Basically it is your suggestion, minus some of the generalisations of the task. WDYS?

cjohansen08:02:34

@U0ETXRFEW du er på rett vei! Jeg vet ikke hva din :store/assoc-in gjør, men jeg ville tenkt at den burde si noe om verdien også, ikke bare pathen. Ellers er målet med denne måten å kode på at du får generiske UI-komponenter som kun representerer designet ditt, og kan gjenbrukes til mange typer data. For å få til det bør detaljer sånn som store-path sendes inn til komponenten. Anbefaler ellers bloggposten til @U07FCNURX 😊

pez09:02:51

Tack så mycket, båda två. Jag kommer ha stor nytta av det här samtalet. @U9MKYDN4Q, kan du utveckla frågan om store-eventet borde säga något om världen? Mitt problem här var initialt att jag inte hittade ett snyggt sätt att ge knappen kunskap om det. Så lösningen blev att låta eventhanteraren plocka ut det. Känns ovant, re-frame-hanterare sällan tillgång till dom-event. Dumdom verkar bjuda in till det. Jag har inget mål just nu att göra generiska komponenter, men det borde jag kanske ha.

cjohansen09:02:10

@U0ETXRFEW Så du forslaget mitt over? Der har du bare en placeholder for verdien i action-dataene, og så plukkes den faktiske verdien ut av eventet i "maskineriet". Altså:

(def store (atom {}))

(defn process-event-data [actions]
  (doseq [[action & args] actions]
    (case action
      :action/assoc-in (apply swap! store assoc-in args))))

(dd/set-event-handler!
 (fn [e data]
   (->> data
        (walk/postwalk
         (fn [x]
           (cond
             (= x :event/target.value)
             (some-> e .-target .-value)

             :else x)))
        process-actions)))

pez10:02:33

Det var det exemplet som gjorde att jag vågade. 😀 jag förstår det bättre nu när jag tagit några stapplande steg i den riktningen.

leifericf09:02:20

Hei, dere! Er det noen her som er på jakt etter Clojure-jobb for tida? Jeg har en på tråden som leter etter folk, som fremstår som seriøs. > I am not contacting you regarding a position to recruit you for (as it looks to me like you just started a new position at a fun company), rather I was hoping to enlist any guidance you may have to offer regarding a Clojure Backend Developer given that you head up the clojure meetup group in Oslo. > > It has been exceptionally difficult to find someone that has experience working with a functional programming language, such as Clojure - that is a backend developer. I didn't know if you had anyone from previous experiences that might have a real passion for this technology & Ruby Rails, that may be looking for this type of role.

cjohansen10:02:32

Lurer på hva vedkommende egentlig ser etter når de spesifiserer så nøye at det skal være en backend-utvikler

👍 1
cjohansen10:02:08

Jeg antar at de leter etter noen spesifikke kvalifikasjoner, og ikke prøver å unngå folk som kan noe om frontend? 😄

augustl10:02:16

og at de må ha erfaring fra Clojure spesifikt, er vel mang en utvikler som er på utkikk etter sin første Clojure-erfaring vil jeg tro 🙂 (edit: hot take redacted, de sa et språk “such as Clojure”)

cjohansen10:02:59

Kan godt skjønne at de gjerne vil ha noen som har den funksjonelle skolesekken

👍 3
teodorlu11:02:31

Jeg er litt rådvill på en sak, ønsker innspill. 1. Jeg har skrevet en Playwright-test i Clojure som jeg er fornøyd med. 2. Testen går gjennom 130 URL-er og sjekker at nettsiden oppfører seg som forventet. 3. Den tar i dag fire minutter å kjøre. 4. Jeg ønsker å parallellisere. Jeg tenker å prøve å sjekke 20 URL-er samtidig. 5. Flaskehalsen er å vente på nettverket og minnebruk for Playwright, ikke tallknusing på CPU. Jeg tror ikke pmap er det jeg vil ha, fordi jeg ønsker å ha kontroll på minnebruken. Jeg tror det jeg ønsker er en arbeidskø foran, 20 workers som tar arbeid fra køen, og en resultatkø på andre siden med ferdigtygget rapport om hvordan det gikk. Hvor ville du startet?

augustl11:02:45

core.async! 20 channels som consumer fra en hoved-channel som er køen over jobber som skal utføres

👍 1
teodorlu12:02:25

ahh, nydelig. Tusen takk!

teodorlu12:02:49

(det ble “fyr av melding og gå til lunsj her”, kanskje dårlig slack-skikk. Skal prøve og komme tilbake til dere når jeg har prøvd!)

leifericf13:02:29

Jeg stilte et lignende spørsmål for en liten stund siden. Kanskje det kan være noe nyttig for deg i den tråden også 🙂 https://clojurians.slack.com/archives/C053AK3F9/p1706446932485359

👍 1
terjesb10:02:49

Hvis url-ene ikke må sjekkes i en spesiell rekkefølge, ville jeg i akkurat dette tilfellet holdt meg unna async-makroer, threadpools og køer, og begynt med noe slikt:

(def *result (atom {}))
  (defn process-url [url result]
    (let [response-time (rand-int 400)
          status (rand-nth [200 404])
          #_#_ {:keys [response-time status]} (hato/get url)]
      (Thread/sleep (java.time.Duration/ofMillis response-time))
      (swap! result assoc url {:response-time response-time
                               :status status})))
  (let [urls (range 130)
        concurrency 20
        semaphore (java.util.concurrent.Semaphore. concurrency)]
    (->> urls
         (run! #(Thread/startVirtualThread
                 (fn []
                   (try
                     (.acquire semaphore)
                     (process-url % *result)
                     (finally
                       (.release semaphore))))))))
Her fyrer vi opp en virtuell tråd per url “Erlang-style”(?), lar 20 om gangen jobbe, og putter resultatet løpende inn i et atom. Forutsetter at du er på Java 21:) (Hvis jeg ikke drømte, hørte jeg noe på en podcast nylig om at man vurderer å skrive om core.async til å bruke virtual threads under the hood istedenfor dagens makro-magi(?).)

1
👍 1
👀 1
teodorlu10:02:15

Interessant grep å bruke tilfeldighet, ja. Får inntrykk at det tar oss inn i property-testing-territorium?

terjesb10:02:15

Hva mener du med å unngå sjekke url to ganger? Den “køer” jo hver url fra urls én gang.

teodorlu10:02:21

Edit: jeg leste helt feil, det er jo ikke det du gjør. Sorry!

👍 1
teodorlu10:02:10

Og ja, jeg er på Java 21. Har ønsket å sette meg inn i virtual threads også.

👌 1
teodorlu10:02:05

> (Hvis jeg ikke drømte, hørte jeg noe på en podcast nylig om at man vurderer å skrive om core.async til å bruke virtual threads under the hood istedenfor dagens makro-magi(?).) (edited) Hvis det viser seg at du ikke drømte og tilfeldigvis finner den podkasten, er jeg interessert i å høre den jeg og! 😄

terjesb10:02:51

Må sjekke:) Tror isf det var Alex Miller som var på Apropos og snakket om Clojure 1.12.

👍 1
teodorlu10:02:15

vet du om Java 21 virtual threads har samme mulighet til å “liste opp hva som kjører nå” som Erlang har? Såvidt jeg vet kan man kjøre en form for “ls” på kjørende tråder i erlang og faktisk drepe prosesser — så de ikke bare blir “borte i bakgrunnen”. (spør av ignoranse, jeg har aldri brukt Erlang)

terjesb10:02:06

Det fine her er at du kan bruke din vanlige synkrone http-klient inne i process-url:

(let [{:keys [response-time status]} (hato/get url)]
 ..)

👍 1
terjesb10:02:26

Tror noe i den retningen kommer delvis via Structured Concurrency, som er i preview. Ser meget interessant ut. https://openjdk.org/jeps/453

👀 2
👍 2
🙏 1
teodorlu10:02:57

fy søren så spennende. Dette var dagens godnyhet! 😀

💯 1
teodorlu10:02:58

Synes det er utrolig tøft at java-folka fremdeles jobber med ting som dette. Ikke “la la la-land, java er best”, men “dere, her har Erlang gjort noe lurt. Skal vi tilby noe på JVM-en som gir noe tilsvarende?”

🙂 1
terjesb11:02:02

(Apropos Apropos, så er det https://vimeo.com/884772901#t=1:12:0, og cirka 1:12:0 ut i den. Fra https://vimeo.com/884772901#t=1:24:50 cirka 1:24:0 snakker de om c.async og virtual threads. Hele episoden er interessant ift Java interop!)

👀 1
teodorlu11:02:29

oooh, nice! Mange takk 🙏

terjesb11:02:27

Ulempen med approachen her er å vite når alle 130 er klare. Akkurat nå vet du ikke hvor lenge du må vente til (count @*result) er 130. Structured concurrency kan være en løsning. Kanskje det her kunne vært en agent istedenfor et resultat-atom. Men jeg hadde begynt med noe slikt:

(def *result (atom {}))
  (def done (promise))

  (defn process-url [url result]
    (let [response-time (rand-int 400)
          status (rand-nth [200 404])
          #_#_ {:keys [response-time status]} (hato/get url)]
      (Thread/sleep (java.time.Duration/ofMillis response-time))
      (swap! result assoc url {:response-time response-time
                               :status status})))
  (let [urls (range 130)
        concurrency 20
        semaphore (java.util.concurrent.Semaphore. concurrency)
        latch (java.util.concurrent.CountDownLatch. (count urls))]
    (->> urls
         (run! #(Thread/startVirtualThread
                 (fn []
                   (try
                     (.acquire semaphore)
                     (process-url % *result)
                     (finally
                       (.countDown latch)
                       (.release semaphore)))))))
    (.await latch)
    (deliver done :done))

👀 1
teodorlu11:02:53

Jaa, så kan jeg bare vente på done i koden som skal fortsette på utsiden?

terjesb11:02:02

Yes!

👍 1
🙌 1
terjesb11:02:05

• eventuelt returnere latch, og kjøre (.await latch) på utsiden • eller få inn en promise som man kjører deliver på • eller returnere en promise, som man kjører deliver på kanskje noe slikt?

(defn process-urls [concurrency result urls]
    (let [semaphore (java.util.concurrent.Semaphore. concurrency)
          latch (java.util.concurrent.CountDownLatch. (count urls))
          p (promise)]

      ;; start én virtual thread per url
      (->> urls
           (run! #(Thread/startVirtualThread
                   (fn []
                     (try
                       (.acquire semaphore)
                       (process-url % result)
                       (finally
                         (.countDown latch)
                         (.release semaphore)))))))

      ;; start en virtual thread som venter på alle jobbene og sier "ferdig"
      (Thread/startVirtualThread
       (fn []
         (.await latch)
         (deliver p :done)))

      ;; returner promise, som blir levert når alt er klart
      p))
Og så: @(process-urls 20 *result (range 300)) (Structured concurrency i preview vil kunne erstatte dette med å holde styr på jobbene og sluttresultatet, men eksemplet her kan funke i mellomtiden.)

1
terjesb11:02:42

Eventuelt:

(defn process-vthread [concurrency result f coll]
    (let [semaphore (java.util.concurrent.Semaphore. concurrency)
          latch (java.util.concurrent.CountDownLatch. (count coll))
          p (promise)]

      ;; start én virtual thread per element
      (->> coll
           (run! #(Thread/startVirtualThread
                   (fn []
                     (try
                       (.acquire semaphore)
                       (f % result)
                       (finally
                         (.countDown latch)
                         (.release semaphore)))))))

      ;; start en virtual thread som venter på alle jobbene og sier "ferdig"
      (Thread/startVirtualThread
       (fn []
         (.await latch)
         (deliver p :done)))

      ;; returner promise, som blir levert når alt er klart
      p))
@(process-vthread 20 *result process-url (range 300))

👀 1
teodorlu12:02:02

Når jeg har skrevet Go-kode tidligere for liknende problem, har jeg typisk tatt inn en kanal som arguemnt som funksjonen har levert “nå er det ferdig!” på. Bruken blir vel ganske lik her.

terjesb12:02:23

Ja. Nåværende funksjon kan kalles som fire-and-forget uten promise, hvis man ikke har behov for å vente på alt og derefe promisen. Ellers bare å ta inn p utenfra istedenfor, så har du Go-mønsteret:)

👍 1
teodorlu14:02:00

@U0523NZUF Jeg ville bare si tusen takk en gang til, koden du kom med var til stor hjelp. Jeg har endelig fått satt av nok tid til å skjønne hva som skjer, og endre litt. Vedlagt har jeg prøvd å se på hva som skjer underveis. Blå betyr “venter på å starte”, oransje betyr “under arbeid” og grønn betyr “ferdig”. 🙏

teodorlu14:02:09

Her er et eksempel med pipeline-blocking fra core.async også, tungt basert på et clojuredocs-eksempel. Dette var mye mindre kode enn jeg hadde forventet.

cjohansen14:02:09

Apropos SQL og eleganse: TIL at Oracle støtter ikke limit 😂

slipset14:02:18

Det er jo ikke SQL det er jo Oracles manglende implementasjon av standarden. Makan.

cjohansen14:02:26

Jeg sa ikke at det var SQL sin feil, bare at det var apropos SQL 😄

cjohansen14:02:02

OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY er Oracles elegante løsning btw 😁

leifericf16:02:31

Tror du også kan bruke where ROWNUM <= 10

augustl15:02:00

fetch next X rows only er visstnok det som har blitt standard, LIMIT var noe postgres/mysql la til før standard var etablert 😮

cjohansen15:02:45

ja, det oppdaget jeg også nettopp 😂

cjohansen15:02:52

så da loller jeg litt på vegne av SQL allikevel

Jakub Holý (HolyJak)12:02:19

Med SQL er det litt som med emacs: ingen sin sql/emcas er lik som andres 😹

😂 1
Zeniten15:02:39

Er det noen av dere som har satt opp http-kit med hot-reloading?

1
Zeniten15:02:35

Uten helt å vite hva jeg driver med, forsøker jeg å koble sammen http-kit, reitit.ring og ring.middleware.reload, men det fungerer ikke. 😞 Mitt forsøk så langt:

(ns quiz.main
  (:require
   [hiccup2.core :as h]
   [org.httpkit.server :as hk-server]
   [reitit.ring :as ring]
   [reitit.ring.middleware.dev]
   [ring.middleware.reload :as reload]
   ))

(defn template
  [content]
  (str (h/html [:html
                [:head
                 [:script
                  {:src "",
                   :integrity
                   "sha384-EAzY246d6BpbWR7sQ8+WEm40J8c3dHFsqC58IgPlh4kMbRRI6P6WA+LA/qGAyAu8",
                   :crossorigin "anonymous"}]]
                [:body
                 [:header
                  [:h1 "Martin Quiz!"]]
                 [:main content]]])))

(defn home [_req]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body (template
          [:section
           "test"])})

(def app
  (ring/ring-handler
   (ring/router
    [["/" {:get {:handler home}}]])))

(defn -main []
  (let [handler (reload/wrap-reload #'app)]
    (println "Starting server")
    (hk-server/run-server handler {:port 8080})))

Zeniten16:02:54

Hm ... det fungerer kanskje som det skal: Når jeg lagrer filen og refresher siden, så er endringen fanget opp. Jeg var ute etter at det refreshes automatisk også slik at endringene vises med en gang på siden, som jeg er blitt vant med i forbindelse med React-utvikling.

Zeniten16:02:41

Jeg fant nettopp denne, og jeg skal prøve noe lignende: https://andydote.co.uk/2023/11/15/hot-reload-for-serverside-rendering/

Zeniten10:02:30

https://github.com/weavejester/ring-refresh fungerer, 🎉 (Men man mister vel state.)

teodorlu08:02:05

Sånn jeg har gjort det: 1. Putter Http-kit-serveren i et atom, så jeg kan skru den av og på 2. Med Reitit regenererer jeg Router-tabellen på hver request. Da mister jeg ikke state.

teodorlu08:02:13

MEN det er jo ikke automatikk i at det skjer når jeg lagrer. Jeg jobber fremdeles i en REPL. Så det løser kanskje ikke det du ba om.

Zeniten11:02:59

Nice, takk innspill! Jeg er ganske fornøyd med hvordan det ble til slutt, og jeg tror jeg har løst problemet vedrørende state ved at jeg holder state i et separat navnerom. Når jeg lagrer nå, så reevalueres det gjeldende navnerommet, og nettsiden oppdateres.

👍 1
Zeniten11:02:48

Jeg lurer litt på hva som skjer om jeg splitter koden opp over flere filer, men, for jeg har fremdeles alt i én fil, utenom state-biten.

👍 1
teodorlu11:02:58

Skjønner, ser den. Jeg har lett litt etter løsning på “cirka samme problem”. For meg ble det til at jeg begynte å grave i å bruke #portfolio (som @U9MKYDN4Q har laget). Det går visst ann å bruke Portfolio med en server-side Clojure-app, men å ha et tynt (separat) clojurescript-bygg som kun viser “dumme komponenter”, der komponentene er skrevet i cljc, tar argumenter, og returnerer hiccup. Men jeg satte meg fast i clojurescript-bygg. Jeg gikk for shadow-cljs (siden jeg har brukt shadow før). Så ble det neste dag, så fortsatte jeg med “re-evaluér http handlers i REPL, alt+tab, F5”. (mer info i https://clojurians.slack.com/archives/C053R9G2C4V/p1697279327626169)

👌 1
slipset16:02:49

En ting jeg ikke liker med sql er alle navnet på alle join variantene leftrightinnerfullouterjoin lissom?