This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-10-17
Channels
- # announcements (7)
- # beginners (9)
- # calva (17)
- # clj-kondo (11)
- # clojure (68)
- # clojure-austin (2)
- # clojure-bay-area (3)
- # clojure-europe (19)
- # clojure-gamedev (10)
- # clojure-nl (1)
- # clojure-norway (73)
- # clojure-spec (5)
- # clojure-uk (5)
- # clojuredesign-podcast (6)
- # clojurescript (65)
- # community-development (28)
- # conjure (1)
- # datahike (34)
- # datomic (36)
- # emacs (11)
- # funcool (1)
- # helix (13)
- # honeysql (36)
- # hyperfiddle (15)
- # jobs (1)
- # jobs-discuss (4)
- # malli (13)
- # nbb (21)
- # off-topic (51)
- # practicalli (20)
- # reitit (1)
- # releases (1)
- # ring (4)
- # squint (1)
- # tools-deps (14)
- # transit (8)
god morgen! I går presenterte jeg 1.5 time live koding på Oslo Kotlin Meetup, om hvordan man kan sette opp en web-backend fra bunnen av uten rammeverk (aka boka, aka Clojure-In-Kotlin). Var en god del folk der som virket å bli litt 🤯 av hvor lite som skal til for å få til det. Moro!
her er forøvrig koden vi endte opp med: https://github.com/augustl/2023-10-kotlinmeetup/blob/28f9aeeafe60ef1214daab03ac69df7c89e74c08/src/main/kotlin/kotlinmeetup/Main.kt
Mor….gennøtt! 🐣
(defn map-of-seqs->seq-of-maps-test [map-of-seqs]
;; your code!
)
(deftest map-of-seqs->seq-of-maps-test
(is (= [{:x 1 :y 10} {:x 2 :y 20} {:x 3 :y 30}]
(map-of-seqs->seq-of-maps {:x [1 2 3] :y [10 20 30]}))))
Trengte akkurat denne funksjonen i egen kode. Fikk kodet opp noe til grønn test, men:
• jeg brukte lenger tid enn jeg hadde forventet
• jeg ble ikke særlig fornøyd med koden.
Noen andre som vil prøve? Jeg deler min løsning i løpet av dagen!(defn map-of-seqs->seq-of-maps [x]
(for [values (apply map list (vals x))]
(zipmap (keys x) values)))
@U0ESP0TS8 føler at du traff bedre enn meg, koden min var mindre konsis 🙂
@U3X7174KS erstattet anonym funksjon med list
er det noe risiko for at keys
returnerer keys i forskjellig rekkefølge når man kaller den flere ganger på samme map i Clojure? Magefølelsen min er “nei”, men jeg er litt usikker
fra doc'n til keys
:
> Returns a sequence of the map's keys, in the same order as (seq map).
fra doc'n til vals
:
> Returns a sequence of the map's values, in the same order as (seq map).
Jeg har heller aldri observert noe annet i praksis, så har egentlig ikke noen grunn til å tvile. Man kan vel unngå det ved å kalle seq
på hele greia først, så splitte i keys og values, men da blir mye av elegansen over borte.
seq
garanterer vel egentlig ikke at den returnerer map entries i samme rekkefølge, men ja
(let [{:keys [x y]} {:x [1 2 3] :y [10 20 30]}]
(->> (interleave x y)
(partition 2)
(map (fn [[x' y']] {:x x' :y y'}))))
føler det map-kallet kan byttes ut med en av de fancy funksjonene i stdlib jeg ikke kan. Kanskje juxt elns
Den var kompakt! Koden blir mer eksplisitt når du snakker om x-er og y-er. Men da støtter den heller ikke z-er, skulle de dukke opp.
(defn map-of-seqs->seq-of-maps [map-of-seqs]
(into []
(mapcat
(fn [[k vs]]
(for [v vs]
{k v})))
map-of-seqs))
@U06BEJGKD har du testet funksjonen din? 😅
en tilsvarende løsning som min forrige, som støtter N keys
(let [x {:x [1 2 3] :y [10 20 30] :z ['a 'b 'c]}]
(->> (apply interleave (vals x))
(partition (count x))
(map (fn [xs] (zipmap (keys x) xs)))))
MORE THREADING
(let [x {:x [1 2 3] :y [10 20 30] :z ['a 'b 'c]}]
(->> (vals x)
(apply interleave)
(partition (count x))
(map (fn [xs] (zipmap (keys x) xs)))))
Her er min egen! Fikk det først ikke til. Så trodde jeg jeg hadde fått det til, men jeg ga feil resultat. Da skrev jeg en test! Liker både løsningen til @U0ESP0TS8 og løsningen til @U0MKRS1FX bedre enn min. Fant ikke noe godt navn på “applesauce”.
(defn map-of-seqs->seq-of-maps [map-of-seqs]
(let [ks (keys map-of-seqs)
applesauce (apply map vector (map #(get map-of-seqs %) ks))]
(map (fn [tuple] (zipmap ks tuple))
applesauce)))
En litt annen tilnærming:
(defn map-of-seqs->seq-of-maps [m]
(when (some seq (vals m))
(cons (update-vals m first)
(map-of-seqs->seq-of-maps (update-vals m next)))))
Nøsta maps fort blir så vanskelig å kompilere mentalt.@U024X3V2YN4 lurt! Jeg klarer å lese koden din lettere enn min egen 😅
@U3X7174KS her er din løsning med threading macro, så slipper man å gi navn på ting 😄
(let [map-of-seqs {:x [1 2 3] :y [10 20 30] :z ['a 'b 'c]}]
(let [ks (keys map-of-seqs)]
(->> ks
(map #(get map-of-seqs %))
(apply map vector)
(map #(zipmap ks %)))))
@U3X7174KS Den anonyme funksjonen er faktisk unøvdvending siden hash-maps er sjøl funksjoner, så det kan skrives
(let [map-of-seqs {:x [1 2 3] :y [10 20 30] :z ['a 'b 'c]}]
(let [ks (keys map-of-seqs)]
(->> ks
(map map-of-seqs)
(apply map vector)
(map #(zipmap ks %)))))
Men så vet vi også at (map m (keys m))
er det samme som (vals m)
:
(let [map-of-seqs {:x [1 2 3] :y [10 20 30] :z ['a 'b 'c]}]
(let [ks (keys map-of-seqs)]
(->> (vals map-of-seqs)
(apply map vector)
(map #(zipmap ks %)))))
Som er egentlig det samme som @U0ESP0TS8 sin første løsningkanskje det er forsvarlig å bruke partial
her også?
(let [map-of-seqs {:x [1 2 3] :y [10 20 30] :z ['a 'b 'c]}]
(let [ks (keys map-of-seqs)]
(->> (vals map-of-seqs)
(apply map vector)
(map (partial zipmap ks)))))
Jeg er ofte skeptisk til å bruke for mye partial
og comp
siden ting lett blir veldig abstrakt.
Edit: etter å ha sett litt på partial-koden synes jeg egentlig den er litt dårligere.Jeg har forsøkt å rydde opp litt i handlers
namespace mitt i dag tidlig:
(ns handlers
(:require [hiccup2.core :as h]))
(def http-headers-default {"Content-Type" "text/html; charset=UTF-8"})
(def page-head-default
[[:meta {:charset "utf-8"}]
[:meta {:name "viewport"
:content "width=device-width, initial-scale=1"}]
[:title "My App"]
[:link {:rel "stylesheet"
:href ""}]])
(defn build-page
([page-body]
(build-page page-body page-head-default))
([page-body page-head]
[:html
[:head (seq page-head)]
[:body (seq page-body)]]))
(defn hiccup-html->str [hiccup]
(-> hiccup (h/html) (str)))
(defn home []
(let [page-body [[:section.section
[:div.container
[:h1.title "Front Page"]
[:p.subtitle "You are now on the front page."]]]]]
{:status 200
:headers http-headers-default
:body (-> page-body
build-page
hiccup-html->str)}))
(defn greet [request]
(let [page-body [[:section.section
[:div.container [:h1.title "Greeting Page"]
[:p.subtitle "Hello, " (get-in request [:params "name"]) "!"]]]]]
{:status 200
:headers http-headers-default
:body (-> page-body
build-page
hiccup-html->str)}))
(defn not-found []
(let [page-body [[:section.section
[:div.container [:h1.title "Page Not Found"]
[:p.subtitle "This page does not exist."]]]]]
{:status 404
:headers http-headers-default
:body (-> page-body
build-page
hiccup-html->str)}))
I handlers kunne jeg bruke en vector av vectors istedenfor en liste av vectors hvis jeg bruker seq
for å "pakke ut" body i build-page
funksjonen.
Nå inneholder ikke build-page
noe innhold, kun "HTML ramma," og man kan sende inn en optional header for å overstyre default.
Og nå kommer alltid hele page-body
fra respektive handlers. Jeg pønsker litt på om jeg bør flytte selve innholdet til separate .edn
filer :thinking_face:Jeg tror jeg foretrekker å ha innholdet i et separat content
namespace. Da ser det litt røddigere ut i handlers
namespacet:
(ns handlers
(:require [content :as c]
[hiccup2.core :as h]))
(def http-headers-default {"Content-Type" "text/html; charset=UTF-8"})
(defn build-page
([page-body]
(build-page page-body c/default-head))
([page-body page-head]
[:html
[:head (seq page-head)]
[:body (seq page-body)]]))
(defn hiccup-html->str [hiccup]
(-> hiccup (h/html) (str)))
(defn home []
{:status 200
:headers http-headers-default
:body (-> c/home-body
build-page
hiccup-html->str)})
(defn greet [request]
{:status 200
:headers http-headers-default
:body (-> (c/build-greet-body (get-in request [:params "name"]))
build-page
hiccup-html->str)})
(defn not-found []
{:status 404
:headers http-headers-default
:body (-> c/not-found-body
build-page
hiccup-html->str)})
content
namespacet:
(ns content)
(def default-head
[[:meta {:charset "utf-8"}]
[:meta {:name "viewport"
:content "width=device-width, initial-scale=1"}]
[:title "My App"]
[:link {:rel "stylesheet"
:href ""}]])
(def home-body
[[:section.section
[:div.container
[:h1.title "Front Page"]
[:p.subtitle "You are now on the front page."]]]])
(defn build-greet-body [name]
[[:section.section
[:div.container [:h1.title "Greeting Page"]
[:p.subtitle "Hello, " name "!"]]]])
(def not-found-body
[[:section.section
[:div.container [:h1.title "Page Not Found"]
[:p.subtitle "This page does not exist."]]]])
Hiccup, som du bruker, har denne fine makroen html
som pre-kompilerer hiccup til strenger. Jeg bruker den via min egen makro (for å gi den et kortere navn, og for å ha den tilgjengelig fra et ns med mange andre nyttige funksjoner, ikke vist her):
(ns web
(:require [hiccup2.core :as hiccup]))
(defmacro h
[content]
(list `hiccup/html content))
Så, i ditt content
-ns kan du bruke den slik:
(ns content
(:require [web :refer [h]]))
(defn build-greet-body [name]
(h
[:section.section
[:div.container [:h1.title "Greeting Page"]
[:p.subtitle "Hello, " name "!"]]]))
Det fiffige med denne makroen er at den pre-kompilerer hiccup til strenger, slik at du betaler kostnaden med rendering for det meste ved kompilering, i stedet for under kjøretid.
Sjekk her:
(macroexpand
`(h
[:section.section
[:div.container [:h1.title "Greeting Page"]
[:p.subtitle "Hello, " name "!"]]]))
=>
(hiccup.util/raw-string
(clojure.core/str
"<section"
" class=\"section\""
">"
"<div"
" class=\"container\""
">"
"<h1 class=\"title\">Greeting Page</h1>"
"<p"
" class=\"subtitle\""
">"
"Hello, "
(hiccup.compiler/render-html clojure.core/name)
"!"
"</p>"
"</div>"
"</section>"))
(Du ser clojure.core/name
her fordi name
er bundet til funksjonen name
i clojure.core
akkurat da jeg kalte macroexpand
, men i en funksjon vil dette navnet selvfølgelig være bundet til funksjonsargumentet name
.)Du kan selvfølgelig leve et helt fint og ukomplisert liv uten denne typen optimalisering! 🙂
Ellers vil jeg si at det kanskje er mer idiomatisk Hiccup å skrive
(list
[:div.foo "foo"]
[:div.bar "bar"])
Enn:
[[:div.foo "foo"]
[:div.bar "bar"]]
Da slipper du også seq
-kallene dine i build-page
.Andre hiccup-bibliotek har popularisert :<>
til dette formålet, men Hiccup (med stor H) støtter ikke dette:
[:<>
[:div.foo "foo"]
[:div.bar "bar"]]
Kult, takk for tipsene, @U06BEJGKD!
Jeg bruker faktisk hiccup2.core/html
i mitt handlers
namespace (se koden i min opprinnelige post, i funksjonen jeg har gitt det noe kryptiske navnet hiccup-html->str
). Men det er kanskje feil sted å gjøre sånt når jeg tenker meg om. Hmmm. Jeg tenkte ikke på å lage en makro! Har ikke vært innom det temaet enda. Kult eksempel du har!
Interessant at du nevner det med list
vs. vector literals. Jeg brukte lists
i starten. Kanskje jeg går tilbake til det, da! 😁
Du må bruke hiccup2.core/html
i alle HTML-produserende funksjoner for at den skal prekompilere (den “trår ikke over” funksjonskallgrenser). Og du må bruke den i den “ytterste” funksjonen, etterfulgt av str
, slik du gjør, for at den skal produsere en streng. Men dette er som sagt en optimalisering, og ikke akkurat nødvendig.
Ja, jeg prøvde å flytte funksjonen hiccup-html->str
inn i mitt content
namespace tidligere, og da ble det bare lok.
Ja, den må være helt ytterst. “Innenfor” der kan velge å bare jobbe med hiccup, altså vektorer (eventuelt i lister, som ovenfor). Og så kan du optimalisere med prekompilering, ved hjelp av den samme hiccup2.core/html
hvis du trenger det.
Kult. Det var relativt enkelt å bruke HTMX også, ja.
sever
namespace (routes):
(defn app [req]
(case (:uri req)
"/htmx-demo" (handlers/htmx-demo)
"/htmx-demo-replace-button" (handlers/htmx-demo-replace-button)
(h/not-found)))
handlers
namespace:
(defn htmx-demo []
{:status 200
:headers http-headers-default
:body (-> content/htmx-demo
build-page
hiccup-html->str)})
(defn htmx-demo-replace-button []
{:status 200
:headers http-headers-default
:body (hiccup-html->str [:p "Thank You!"])})
content
namespace:
(def default-head
(list [:meta {:charset "utf-8"}]
[:meta {:name "viewport"
:content "width=device-width, initial-scale=1"}]
[:title "My App"]
[:link {:rel "stylesheet"
:href ""}]
[:script {:src ""}]))
(def htmx-demo
[:section.section
[:div.container
[:h1.title "HTMX Demo"]
[:button.button {:hx-post "/htmx-demo-replace-button" :hx-swap "outerHTML"} "Click Me!"]]])
That's it!Ja, tøft! Jeg ble litt paff over hvor lite som skulle til. Herlig at du utforsker dette og deler, @leif.eric.fredheim!
Jeg lagde en todo-app med HTMX. Syns også det var ganske kjekt 👍 Koden ble litt overkomplisert fordi jeg eksperimenterte med å ha sånn “progressive enhancement” hvor appen vil fungere uten javascript også (merk bruken av “hx-boost” i forms’ene), ville nok sett mer elegant ut uten! https://github.com/Oddsor/clojure-web-backend-template/blob/main/examples/todo-app/src/todo_app/core.clj
Spesielt fornøyd med at man kan implementere ting som input-fokus og at alt er valgt on-fokus uten nesten noe javascript (annet enn hele HTMX da 😬 )
Liker også at man kan legge til elementer som skal insertes andre steder (det er slik jeg nullstiller skjemaet for å legge til oppgave), eller at man alternativt kan dispatche events for å trigge kall andre steder. Kanskje interessant for deg også, @leif.eric.fredheim? Out of band swap: https://htmx.org/docs/#oob_swaps https://github.com/Oddsor/clojure-web-backend-template/blob/bfe9d89145a0b782247c9aefba0059cc5058a420/examples/todo-app/src/todo_app/core.clj#L114 Events: https://htmx.org/docs/#the-hx-on-attribute https://github.com/Oddsor/clojure-web-backend-template/blob/bfe9d89145a0b782247c9aefba0059cc5058a420/examples/todo-app/src/todo_app/core.clj#L85
Hei @UDB2Q0W13, hilsen fra Island. Fikk todo appen til a kjöre. Hvis jeg önsker a bruke den template og utvikle i IntelliJ, hvordan faar jeg det til?
Hei! Jeg kan dessverre ikke hjelpe med IntelliJ siden jeg ikke har brukt det på en stund, men hvis det er noe likt flyten i Calva så har jeg lagt ved en video som viser hvordan jeg starter opp appen for utvikling iallefall! Man må være nødt til å kunne starte opp med et alias (`:dev`) slik at dev-mappen med user-namespace blir mounted, så kan man starte appen for lokal utvikling ved å evaluere “reset”-kommandoen.
@leif.eric.fredheim et lite forslag, basert på noe vi fiksa på jobb nylig: Lag en middleware som konverterer hiccup til en streng. Da slipper du for det første å gjøre hiccup-html->str
i alle handlerne dine, og i tillegg blir det lettere å skrive tester på handlerne, ettersom det er lettere å gjøre asserts mot hiccup, som er en datastruktur, enn en streng. Noe ala:
(defn get-content-type [{:keys [headers]}]
(when-let [k (->> (keys headers)
(filter (comp #{"content-type"} clojure.string/lower-case))
first)]
(get headers k)))
(defn html-response? [res]
(some->> (get-content-type res)
(re-find #"text/html")))
(defn hiccup? [x]
(or (and (vector? x)
(keyword? (first x)))
(and (coll? x)
(every? hiccup? x))))
(defn wrap-hiccup-response [handler]
(fn [req]
(let [res (handler req)]
(cond-> res
(and (html-response? res)
(hiccup? (:body res)))
(update :body #(str (h/html %)))))))
(defn start []
(stop)
(reset! server (-> #'app
wrap-params
wrap-hiccup-response
(hk/run-server {:port 3001}))))
Apropos tester mot hiccup så skrev jeg denne for mange herrans år siden: https://github.com/cjohansen/hiccup-find
Heftig! Tusen takk for rådet og info om testing, @U9MKYDN4Q
den hiccup-sjekken mot body er egentlig god nok til at du kan kutte sjekken på content-type, og heller legge på headeren også i middlewaren om du så skulle ønske
Jeg kopierte koden din rått og det fungerte på første forsøk uten issues. Smooth. Nå må jeg bare lese nøye for å forstå alt som skjer der, hehe.
Det var ikke vanskelig å forstå! Jeg slet litt med disse forms bare:
(filter (comp #{"content-type"} s/lower-case))
comp
er grei, men #{"content-type"}
synes jeg var litt rar. Jeg forstår hva som skjer, men ikke helt hvordan det funker.
(re-find #"text/html")
#"text/html"
var litt rar syntax, før jeg kom på at det er sånn regex literals ser ut. Jeg leste først re-find
med "re" som i "repeat," altså "find it again," og ikke "re" som i "regex," som det antagelig betyr 😅
(update :body #(str (h/html %)))
update
hadde jeg ikke sett før, men det var greit å forstå etter å ha lest litt om funksjonen.
(filter (comp #{"content-type"} s/lower-case))
Comp kaller funksjonene fra høyre til venstre. Først gjør om til lowercase, deretter test verdien for medlemsskap i settet #{"content-type"}
- med andre ord, sjekk om strengen er lik den
å skrive færre egne funksjoner som lener seg mer på innebygde funksjoner kan jeg lære én og annen ting om også. > Med andre ord: Når byggeklossene er gode nok, kan man øke lesbarheten ved å unngå navngitte funksjoner. 💯
> […] test verdien for medlemsskap i settet #{"content-type"}
Aha! Da falt det på plass. Det var den biten jeg ikke forstod.
Et set
i Clojure er en funksjon - de implementerer IFn
og kan brukes som en funksjon som tar ett argumentet. Denne funksjonen slår opp argumentet i “seg selv” (altså settet), og returnerer denne verdien, eller nil
hvis verdien ikke fins i settet. Dette er et mye brukt idiom for å teste om en verdi er lik én eller flere andre verdier.
Uten denne teknikken ville det blitt:
(filter (comp #(= "content-type" %) s/lower-case))
Ahaaa, kult! Litt som måten et keyword kan brukes som en funksjon til å slå opp seg selv i et map.
Nettop, og keywords kan også brukes på mengder: (:foo #{:foo :bar}) => :foo
, her er et eksempel som illustrerer både comp
og keywords brukt på hash-maps og mengder
(let [foo {:name "foo"
:skills #{:typing :singing :sprinting}}
bar {:name "bar"
:skills #{:sprinting :knitting}}
baz {:name "baz"
:skills #{:singing :whittling}}]
;; find those who can sprint
(filter (comp :sprinting :skills) [foo bar baz]))