clojure.core/keep forklart med monader
Monader, kanskje det dårligst forklarte og mest forvirrende computer science-begrepet på internett!
“Det er en burrito”, hva pokker?
(“Monader er burritoer” er en faktisk forklaring folk har prøvd seg på, prøv et google-søk).
Og hva i all verden har monader med keep
fra clojure.core
å gjøre?
Jeg liker utfordringer, så jeg tenkte jeg skulle prøve meg på å forklare keep med monader, og dermed også forklare monader. Dagens tekst tar deg gjennom litt “type først”-tankegods, før vi returnerer til Clojure for å få eksempler i REPL:
- Lynkurs i lesing av Haskell-typer
- Monader, forklart med Haskell-typer
- Monadiske operasjoner med Clojure-data
- Til slutt, hva er egentlig
clojure.core/keep
?
Lynkurs i Haskell-typer
Før vi kan forklare monader med typer, må vi trene på å lese typer. Og for å være presise, snakker vi om monader som omtalt i Haskell-økosystemet.
I Haskell putter du typen bak ::
:
age :: Int
age = 3
Funksjoner av ett argument får en pil (->
) i signaturen sin.
increase :: Int -> Int
increase x = x + 1
Funksjoner av to eller flere argumenter får to eller flere piler i signaturen. Det har en kul teknisk forklaring som er langt utenfor fokuset til denne teksten¹.
average :: Double -> Double -> Double
average x y = (x + y) / 2
Lister i Haskell likner mistenkelig på JSON-arrays:
imdbRatings :: [Double]
imdbRatings = [8.2, 9.1, 8.4]
-- bonus-spørsmål: hvilke tre filmer er dette fra?
… og i stedet for en magisk verdi som er ingenting, men er av alle typer, er “verdi som kanskje er tom” en eksplisitt type:
envisioningInformationAuthor :: Maybe String
envisioningInformationAuthor = Just "Edvard Tufte"
bhagavadGitaAuthor :: Maybe String
bhagavadGitaAuthor = Nothing
-- de fleste bøker er skrevet av en forfatter, men det er ikke
-- alltid like lett å peke til én person.
Sånn! Da kan vi nok om Haskell-typer til å forklare monader.
Typesignatur for API-kontrakten “monade”
En monade er en API-kontrakt for en type. API-kontrakten krever at du implementerer to funksjoner.
Den første funksjonen er return
.
return
tar en verdi, og gir verdien pakket inn i den monadiske typen.
La oss implementere den for lister og maybe.
returnList :: a -> [a]
returnList x = [x]
returnMaybe :: a -> Maybe a
returnMaybe x = Just x
Hvis du lurer på hvor a
kommer fra, er dette en ubrukt type, som vi heller ikke går inn på i dag.
Før vi forklarer den andre funksjonen i API-kontrakten for monader, må vi lære oss å evaluere eksempelkode i Haskell.
Det gjør vi med programmet ghci
.
ghci
også kjent som GHCi er kort for “GHC Interactive”, igjen kort for “Glasgow Haskell Compiler Interactive” (fordi kompilatoren ble laget i Skottland).
$ ghci
GHCi, version 9.12.2: https://www.haskell.org/ghc/ :? for help
ghci> 1 + 2
3
GHCi lar deg enten evaluere Haskell, eller gjøre andre ting.
Andre ting skal prefikses med kolon.
Den ene andre tingen vi bryr oss om i dag, er “gi meg typen til”, med :t
eller :type
.
ghci> :t map
map :: (a -> b) -> [a] -> [b]
ghci> :type map
map :: (a -> b) -> [a] -> [b]
For å få typen til en infix-operator, må vi omringe operatoren med parenteser.
ghci> :t +
<interactive>:1:1: error: [GHC-58481]
parse error on input ‘+’
ghci> :t (+)
(+) :: Num a => a -> a -> a
Tilbake til planlagt sending.
Den første monade-funksjonen heter return
, den andre kalles bind.
Bind brukes med infix-operatoren >>=
.
ghci> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Her får vi tre ubrukte typer, m
, a
og b
.
Vi kan få vekk m
ved å spesialisere bind til lister og maybe.
bindList :: [a] -> (a -> [b]) -> [b]
bindList = bind
bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe = bind
Eksempler på API-kontrakten “monade”
Jeg foretrekker forklaringer med eksempler, da får jeg noe håndfast å forholde meg til. Maybe og liste er konkrete monader. Nå skal vi også få konkrete eksempler på bruk av monade-operasjonen.
return
er lett:
ghci> returnList
[]
ghci> returnMaybe
Nothing
Tom liste for lister og en “ingenting” for Maybe.
Bind gjør noe mer spennende. Typen til bind avdekker at ett av argumentene er en funksjon.
ghci> :t bindList
bindList :: [a] -> (a -> [b]) -> [b]
-- ^
-- funksjon fra a til liste av b
“fra ett element til liste av mange elementer”? Hmm.
Når trenger vi det, mon tro?
adjectivize :: String -> [String]
adjectivize s = map (\a -> a ++ " " ++ s) ["The fabulous", "The ingenious", "The completely dreamy-eyed"]
Du kan ignorere implementasjonen til adjectivize
hvis du vil, essensen ligger i typesignaturen.
ghci> adjectivize "Arne"
["The fabulous Arne","The ingenious Arne","The completely dreamy-eyed Arne"]
Når vi adjektiviserer flere navn, passer signaturen til bindList
!
ghci> mapM_ print $ bindList ["Arne", "Tom", "Tim", "John"] adjectivize
"The fabulous Arne"
"The ingenious Arne"
"The completely dreamy-eyed Arne"
"The fabulous Tom"
"The ingenious Tom"
"The completely dreamy-eyed Tom"
"The fabulous Tim"
"The ingenious Tim"
"The completely dreamy-eyed Tim"
"The fabulous John"
"The ingenious John"
"The completely dreamy-eyed John"
(jeg jukser litt og bruker mapM_
, print
og $
for å splitte resultatet over flere linjer, uten videre forklaring).
Lekse:
bindList lar oss “flate ned” to lag med lister.
Maybe-monaden er nyttig for eksempel på heltallsdivisjon med eksplisitte avrundingsfeil.
safeHalf :: Int -> Maybe Int
safeHalf x = let guess = div x 2
in if guess * 2 == x
then Just guess
else Nothing
ghci> safeHalf 10
Just 5
ghci> safeHalf 9
Nothing
Vi kan nå gjøre mange heltallsdivisjoner uten eksplosjon av if-else-mikmakk i koden:
ghci> bindMaybe (safeHalf 100) safeHalf
Just 25
Bind som infix-operator lar oss i tillegg unngå eksplosjon av parenteser.
-- med infix >>= kan vi slenge på mer jobb på slutten:
ghci> (safeHalf 1000) >>= safeHalf >>= safeHalf >>= safeHalf
Nothing
ghci> (safeHalf 2000) >>= safeHalf >>= safeHalf >>= safeHalf
Just 125
-- ... i kontrast til
ghci> bindMaybe (bindMaybe ( bindMaybe (safeHalf 2000) safeHalf ) safeHalf ) safeHalf
Just 125
Lekse:
bindMaybe lar oss “flate ned” to lag med “kanskje-verdier”.
mapcat
er bindList
Nok Haskell for nå? Rich to the Rescue.
(defn adjectivize [s]
(map #(str % " " s)
["The fabulous", "The ingenious", "The completely dreamy-eyed"]))
(adjectivize "Arne")
;; => ("The fabulous Arne" "The ingenious Arne" "The completely dreamy-eyed Arne")
(mapcat adjectivize ["Arne", "Tom", "Tim", "John"])
;; => ("The fabulous Arne"
;; "The ingenious Arne"
;; "The completely dreamy-eyed Arne"
;; "The fabulous Tom"
;; "The ingenious Tom"
;; "The completely dreamy-eyed Tom"
;; "The fabulous Tim"
;; "The ingenious Tim"
;; "The completely dreamy-eyed Tim"
;; "The fabulous John"
;; "The ingenious John"
;; "The completely dreamy-eyed John")
Hadde du puttet en Haskell-typesignatur på mapcat
hadde du fått nettopp typen [a] -> (a -> [b]) -> [b]
.
some->
er nesten bindMaybe
Når vi gjør trygg halvering i Clojure, bruker vi nil
ved manglende verdi.
(defn safe-half [x]
(let [guess (quot x 2)]
(when (= x (* guess 2))
guess)))
(safe-half 10)
;; => 5
(safe-half 9)
;; => nil
Vi bruker some->
som kombinator for safe-half
:
(some-> 100 safe-half safe-half)
;; => 25
… og hvis vi vil kjøre “mange runder”, slipper vi å gjenta en infiks-operator.
(some-> 1000 safe-half safe-half safe-half safe-half)
;; => nil
(some-> 2000 safe-half safe-half safe-half safe-half)
;; => 125
Kan liste-monadisk og maybe-monadisk oppførsel blandes?
Hvis vi skal dele en liste trygt på to, ender vi opp med en liste av maybe-verdier.
ghci> map safeHalf [0..20]
[Just 0,Nothing,Just 1,Nothing,Just 2,Nothing,Just 3,Nothing,Just 4,Nothing,Just 5,Nothing,Just 6,Nothing,Just 7,Nothing,Just 8,Nothing,Just 9,Nothing,Just 10]
Fordi vi blander lister og maybe, kan vi verken bruke bindList eller bindMaybe!
Men la oss ikke gi opp.
maybeToList :: Maybe a -> [a]
maybeToList Nothing = []
maybeToList (Just x) = [x]
Hah, nå kan vi late som at alt er lister! … og da går typene opp ☺️
ghci> bindList [0..20] (maybeToList . safeHalf)
[0,1,2,3,4,5,6,7,8,9,10]
Dette så ut som en nyttig funksjon å ha 🧐
Vi vil ha denne typesignaturen:
mystery :: [a] -> (a -> Maybe b) -> [b]
Implementasjonen har vi nesten allerede!
Vi må bare la være å hardkode safeHalf
.
mystery xs f = bindList xs (maybeToList . f)
Da kan vi skrive om eksemplet vårt!
ghci> mystery [0..20] safeHalf
[0,1,2,3,4,5,6,7,8,9,10]
🔥
… men disse tankene har andre folk tenkt før …
mystery
-funksjonen finnes såklart allerede i Haskell.
Ved å søke opp typesignaturen i Hoogle,
finner vi Data.Maybe.mapMaybe
.
ghci> import Data.Maybe as M
ghci> :t M.mapMaybe
M.mapMaybe :: (a -> Maybe b) -> [a] -> [b]
Argumentene er byttet om så funksjonen kommer først, men ellers er den lik vår mystery
.
ghci> M.mapMaybe safeHalf [0..20]
[0,1,2,3,4,5,6,7,8,9,10]
… og i Clojure har vi reimplementert keep
.
(keep safe-half (range 0 (inc 20)))
;; => (0 1 2 3 4 5 6 7 8 9 10)
Lærepenger
-
Kanskje-verdier og lister er supre å jobbe med i både Clojure og Haskell. Vi kan si mye med lite kode.
-
Haskell oppfordrer til å tenke i typer, Clojure oppfordrer til å tenke i eksempler. Bruk begge.
-
Når et prinsipp er for abstrakt, spesialiserer til eksempler. Når du har kontroll på eksemplene, løft blikket og gi prinsippene et nytt blikk.
fotnoter:
¹: Start for eksempel på “Higher order functions”-kapittelet i Learn You a Haskell for Great Good: https://learnyouahaskell.com/higher-order-functions#curried-functions