This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-05-16
Channels
- # announcements (1)
- # babashka (13)
- # beginners (33)
- # calva (26)
- # cherry (33)
- # clerk (5)
- # clj-kondo (3)
- # clojure (40)
- # clojure-europe (24)
- # clojure-finland (2)
- # clojure-norway (29)
- # clojurescript (18)
- # cursive (2)
- # datomic (6)
- # docker (11)
- # emacs (12)
- # events (1)
- # fulcro (71)
- # graalvm (8)
- # hyperfiddle (2)
- # lsp (23)
- # meander (5)
- # off-topic (36)
- # polylith (4)
- # re-frame (6)
- # reitit (13)
- # shadow-cljs (87)
- # spacemacs (1)
- # tools-deps (19)
- # vim (5)
- # xtdb (57)
Jeg synes dette var litt gøy. Det var en tanke som slo meg, men som jeg hadde vansker med å formulere.
ble inspirert til å ta en runde med chatgpt4 på monads. Har egentlig ikke blitt noe klokere. Tror sperren min for å “grokke” monads er at jeg aldri føler jeg trenger dem
En monade er kun en abstrakt data type, dvs et interface, og noen “regler” som er ting som ikke går å kode opp i typer, men som man kan teste gjennom unit-tester.
Jeg synes også det er mye lettere å forholde meg til konkrete typer som tilfeldigvis "er monader". Feks lister (tom liste er [], bind er concatMap) og Option (tom option er Empty, bind er "gjør noe hvis vi har en verdi, ikke gjør noe hvis vi har Empty). At dette kan formuleres som en typeklasse / et trait betyr ikke for meg at vi bør gjøre det! Føler også at dette funker best når man jobber med structs og typer. Synes ikke helt det passer i Clojure-koden min.
der er du inne på noe, ja. Jeg har kanskje ikke fasilitetene eller kapasiteten eller ønsket til å forstå hva som er felles med side-effekter, feilhåndtering, async-kode, komponering av funksjoner og seq-prosessering i Clojure
oi, ser ut som jeg allerede er ute på dypt vann. Tok feil av "return" over. Den tar inn et argument, og returnerer "argumentet pakket inn". Så du må kalle den med return "heihei"
, så blir svaret ditt enten Just "heihei"
, ["heihei"]
eller en "liksom IO-effekt" som returnerer "heihei"
når du kjører den.
Her er tre eksempler der "monadisk bind (`>>=`)" er brukt "litt liknende" for "option-monade", "liste-monade" og "IO-monade":
> Just 33 >>= return . (+ 10)
Just 43
> [33] >>= \x -> [(x + 10)]
[43]
> (return 33 :: IO Int) >>= putStrLn . show
33 -- i dette tilfellet er 33 skrevet ut i REPL, ikke returnert
I bildet får dere se hva jeg gjorde feil underveis også 🙂
g . f
i haskell er det samme som (comp g f)
i Clojure.Der der har noe med sideeffekter å gjøre, altså IO monade, og nå er jeg på vei ut på veldig dypt vann er at du kan komponere monadene, men du kan ikke komponere sideeffektene. Jeg har brukt litt tid på å tenke på dette i forbindelse med databaser. Hvis du tenker deg idiotisk javakode:
userDao.findByName("erik");
userDao.findByEmailDomain("");
Disse lar seg ikke komponere (lett):
// gief all users who is called "erik" and has an email-domain being "".
// assume a fn combine. this is bah.
combine(userDao.findByName("erik"), userDao.findByEmailDomain(""))
Hvis du derimot holder deg på query-nivå (switcher nå elegant over til clojure og honeysql:
(defn by-name [n]
[:= :name n])
(defn by-email-domain [d]
[:= :domain d])
Da er combine
triviel:
(defnine combine [& clauses]
(into [:and] clauses))
Og da vil med litt mer magi, (jdbc/execute db q)
være der du utfører effektene dine.Men basically, poenget er å jobbe så lenge som mulig i data land før du sender data til effektsystemet ditt for å få realisert sideeffektene.
Fordi det er mye lettere å komponere beskrivelsen av spørringene enn det er å komponere resultatet av dem.
Og det er her monadene kommer inn, fordi monadene kan komponeres før effektene deres blir kalkulert.
Jeg synes det gir kjempemening å bruke monader når man skal jobbe med masse funksjoner som returnerer null (Maybe-monade i Haskell) eller funksjoner som kan returnere mange svar (liste-monade i Haskell) eller funksjoner som kan feile (Either-monade i Haskell). I Clojure foretrekker jeg som regel bare å jobbe på data. Null punning hjelper masse. Diverse makroer hjelper. Jeg liker jo Clojure bedre enn Haskell personlig.
Jeg synes Eric Normand og Richard Feldman sier mye bra om Haskell og Clojure her: https://www.listennotes.com/podcasts/software-unscripted/haskell-and-clojure-in-FKfvZW_BqPA/
eventuelt går det an å lese https://haskell-hardt-og-godt.pages.dev/#97 jeg lagde på norsk om haskell for lenge siden (og lider litt av endringer i javascript-systemet jeg brukte for å lage slides). Men det er kanskje bedre å lese http://learnyouahaskell.com/a-fistful-of-monads.
Ang maybe drit. Man bør holde bruken av å sjekke på maybe-ness “langt ute” i koden, og heller flatMap’e (er det vel) funksjoner på maybe monaden. Så heller enn:
if (whatever.hasValue()) { // eller hva nå dette heter på monade språket
doSomething(whatever.getValue())
}
så gjør man
whatever.flatMap(doSomething);
Dette er kanskje et dumt spørsmål, men hva er egentlig de praktiske forskjellene mellom "maybe-ness" i Haskell (eller funksjonell programmering mer generelt) og "null-ness" i et OO-språk, som f.eks. C++ eller C#? Er forskjellen hvorvidt verdien "bak" en variabel eksisterer vs. om variabelen ("pekeren") eksisterer i det hele tatt? Har det noe med typesystemer eller "reference types vs. value types" å gjøre?
ChatGPT svarer: > Dette er et godt spørsmål! Forskjellen mellom "maybe-ness" i Haskell (eller funksjonell programmering generelt) og "null-ness" i objektorienterte språk som C++ eller C# har å gjøre med hvordan verdier og variabler behandles. > > I et objektorientert språk kan variabler være null eller peke på en referanse til et objekt som ikke eksisterer (null-referanse). Dette kan føre til nullsjekker og unntakshåndtering for å unngå feil når man prøver å manipulere en null-verdi. Nullsjekking kan ofte være nødvendig for å unngå feil som NullReferenceException i C# eller nullptr i C++. > > I funksjonelle språk som Haskell er null-verdier unngått ved design. I stedet er "maybe-ness" eller "optionality" en måte å representere verdier som kan være til stede eller fraværende på en tryggere måte. Haskell har en innebygd type kalt Maybe som representerer et alternativ som enten kan være en verdi (Just value) eller ingenting (Nothing). Ved å bruke Maybe-type kan man eksempelvis returnere en Maybe-verdi i stedet for null når en funksjon kan mislykkes eller ikke har en gyldig verdi å returnere. > > Forskjellen i håndteringen av "maybe-ness" i funksjonell programmering ligger i hvordan språket tvinger deg til å håndtere alternativer. I Haskell må du møte begge scenariene (Just value og Nothing) og håndtere dem eksplisitt, for eksempel ved hjelp av mønstergjenkjenning. Dette hjelper til med å unngå nullsjekking og potensielle null-relaterte feil som kan oppstå i objektorienterte språk. > > Det er også verdt å merke seg at denne forskjellen er knyttet til typesystemene i språkene. Funksjonelle språk som Haskell har sterk typekontroll og tvinger deg til å håndtere mulige verdier på en mer statisk og typebasert måte. I objektorienterte språk kan nullsjekking være nødvendig på grunn av manglende typeinformasjon og dynamiske natur, spesielt når det kommer til bruk av null-referanser i polymorfe sammenhenger. > > Sammenfattende er forskjellen mellom "maybe-ness" i Haskell og "null-ness" i objektorienterte språk som C++ eller C# i hvordan alternativer og mulige fravær av verdier håndteres. Funksjonell programmering og Haskell tilbyr mekanismer som Maybe-typen for å håndtere dette på en tryggere og mer eksplisitt måte enn null-referanser i objektorienterte språk.
Hvordan er "fravær av verdier" håndtert i Clojure sammenlignet med Haskell, og kan du vise meg noen konkrete kodeeksempler som illustrerer de viktigste forskjellene?
ChatGPT svarer:
I Clojure, et dialekt av Lisp, blir "fravær av verdier" også håndtert på en lignende måte som i Haskell, men med noen forskjeller. I Clojure brukes en spesiell verdi kalt nil
for å representere fravær av en verdi. nil
kan betraktes som en verdi i seg selv og blir brukt som en representasjon av "ingenting" eller "tomhet".
I Haskell bruker vi Maybe-type for å representere alternativer, mens i Clojure bruker vi nil
direkte. La oss se på noen konkrete kodeeksempler for å illustrere forskjellene.
Haskell:
-- Funksjonen `safeDivide` tar to tall som argumenter og returnerer et Maybe-verdi som representerer resultatet av en trygg divisjon.
safeDivide :: Double -> Double -> Maybe Double
safeDivide x 0 = Nothing
safeDivide x y = Just (x / y)
-- Bruk av `safeDivide` og håndtering av Maybe-verdien
divideAndPrint :: Double -> Double -> IO ()
divideAndPrint x y = case safeDivide x y of
Just result -> putStrLn ("Result: " ++ show result)
Nothing -> putStrLn "Cannot divide by zero."
main :: IO ()
main = divideAndPrint 10 5
I dette Haskell-eksempelet definerer vi funksjonen safeDivide
, som tar to tall som argumenter og returnerer en Maybe Double. Hvis den andre verdien er 0, returnerer vi Nothing
for å indikere en ugyldig divisjon. Ellers returnerer vi Just (x / y)
for å indikere et gyldig divisjonsresultat.
Deretter har vi divideAndPrint
som bruker safeDivide
-funksjonen og håndterer Maybe-verdien ved hjelp av en mønstergjenkjenning (case statement). Hvis resultatet er Just result
, skriver vi ut resultatet. Hvis resultatet er Nothing
, skriver vi ut en feilmelding.
Clojure:
; Funksjonen `safe-divide` tar to tall som argumenter og returnerer resultatet av en trygg divisjon.
(defn safe-divide [x y]
(if (zero? y)
nil
(/ x y)))
; Bruk av `safe-divide` og håndtering av `nil`-verdien
(defn divide-and-print [x y]
(let [result (safe-divide x y)]
(if result
(println (str "Result: " result))
(println "Cannot divide by zero."))))
(defn -main []
(divide-and-print 10 5))
I dette Clojure-eksempelet definerer vi funksjonen safe-divide
, som tar to tall som argumenter og returnerer divisjonsresultatet. Hvis den andre verdien er 0, returnerer vi nil
.
Deretter har vi divide-and-print
, som bruker safe-divide
-funksjonen og håndterer `**