tl; dr
{-# LANGUAGE InstanceSigs #-}
newtype Id t = Id t
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
Prologo
L'operatore dell'applicazione $
delle funzioni
forall a b. a -> b
è canonicamente definito
($) :: (a -> b) -> a -> b
f $ x = f x
infixr 0 $
in termini di applicazione della funzione primitiva di Haskell f x
(infixl 10
).
La composizione .
è definita in termini di $
come
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x
infixr 9 .
e soddisfa le equivalenze forall f g h.
f . id = f :: c -> d Right identity
id . g = g :: b -> c Left identity
(f . g) . h = f . (g . h) :: a -> d Associativity
.
è associativo e id
è la sua identità destra e sinistra.
La tripla Kleisli
Nella programmazione, una monade è un costruttore di tipo functor con un'istanza della classe di tipo monade. Esistono diverse varianti equivalenti di definizione e implementazione, ognuna con intuizioni leggermente diverse sull'astrazione della monade.
Un functor è un costruttore f
di tipo di tipo * -> *
con un'istanza della classe di tipo functor.
{-# LANGUAGE KindSignatures #-}
class Functor (f :: * -> *) where
map :: (a -> b) -> (f a -> f b)
Oltre a seguire il protocollo di tipo imposto staticamente, le istanze della classe di tipo functor devono obbedire alle leggi algebriche del funzione forall f g.
map id = id :: f t -> f t Identity
map f . map g = map (f . g) :: f a -> f c Composition / short cut fusion
I calcoli di Functor hanno il tipo
forall f t. Functor f => f t
Un calcolo c r
consiste in risultati r
nel contesto c
.
Le funzioni monadiche unarie o le frecce Kleisli hanno il tipo
forall m a b. Functor m => a -> m b
Le frecce di Kleisi sono funzioni che accettano un argomento a
e restituiscono un calcolo monadicom b
.
Le monadi sono canonicamente definite in termini di tripla di Kleisli forall m. Functor m =>
(m, return, (=<<))
implementato come classe di tipo
class Functor m => Monad m where
return :: t -> m t
(=<<) :: (a -> m b) -> m a -> m b
infixr 1 =<<
L' identità Kleisli return
è una freccia Kleisli che promuove un valore t
nel contesto monadico m
. L'applicazione Extension o Kleisli =<<
applica una freccia Kleisli a -> m b
ai risultati di un calcolom a
.
La composizione di Kleisli <=<
è definita in termini di estensione come
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x
infixr 1 <=<
<=<
compone due frecce Kleisli, applicando la freccia sinistra ai risultati dell'applicazione della freccia destra.
Le istanze della classe del tipo di monade devono obbedire alle leggi della monade , più elegantemente dichiarate in termini di composizione di Kleisli:forall f g h.
f <=< return = f :: c -> m d Right identity
return <=< g = g :: b -> m c Left identity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d Associativity
<=<
è associativo edreturn
è la sua identità destra e sinistra.
Identità
Il tipo di identità
type Id t = t
è la funzione di identità sui tipi
Id :: * -> *
Interpretato come un funzione
return :: t -> Id t
= id :: t -> t
(=<<) :: (a -> Id b) -> Id a -> Id b
= ($) :: (a -> b) -> a -> b
(<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
= (.) :: (b -> c) -> (a -> b) -> (a -> c)
Nel canonico Haskell viene definita la monade dell'identità
newtype Id t = Id t
instance Functor Id where
map :: (a -> b) -> Id a -> Id b
map f (Id x) = Id (f x)
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
Opzione
Un tipo di opzione
data Maybe t = Nothing | Just t
codifica un calcolo Maybe t
che non produce necessariamente un risultato t
, un calcolo che può "fallire". L'opzione monade è definita
instance Functor Maybe where
map :: (a -> b) -> (Maybe a -> Maybe b)
map f (Just x) = Just (f x)
map _ Nothing = Nothing
instance Monad Maybe where
return :: t -> Maybe t
return = Just
(=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
f =<< (Just x) = f x
_ =<< Nothing = Nothing
a -> Maybe b
viene applicato a un risultato solo se Maybe a
produce un risultato.
newtype Nat = Nat Int
I numeri naturali possono essere codificati come numeri interi maggiori o uguali a zero.
toNat :: Int -> Maybe Nat
toNat i | i >= 0 = Just (Nat i)
| otherwise = Nothing
I numeri naturali non vengono chiusi in sottrazione.
(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)
infixl 6 -?
L'opzione monade copre una forma base di gestione delle eccezioni.
(-? 20) <=< toNat :: Int -> Maybe Nat
Elenco
La monade elenco, sopra il tipo di elenco
data [] t = [] | t : [t]
infixr 5 :
e la sua operazione additiva monoid "append"
(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[] ++ ys = ys
infixr 5 ++
codifica il calcolo non lineare[t]
producendo una quantità naturale 0, 1, ...
di risultati t
.
instance Functor [] where
map :: (a -> b) -> ([a] -> [b])
map f (x : xs) = f x : map f xs
map _ [] = []
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> [a] -> [b]
f =<< (x : xs) = f x ++ (f =<< xs)
_ =<< [] = []
L'estensione =<<
concatena ++
tutti gli elenchi [b]
risultanti dalle applicazioni f x
di una freccia Kleisli a -> [b]
in elementi di [a]
un unico elenco di risultati [b]
.
Lasciate che i divisori propri di un numero intero positivo n
essere
divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]
divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)
poi
forall n. let { f = f <=< divisors } in f n = []
Nel definire la classe del tipo monade, anziché l'estensione =<<
, lo standard Haskell usa il suo flip, l' operatore di bind>>=
.
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= \ _ -> k
{-# INLINE (>>) #-}
return :: a -> m a
return = pure
Per semplicità, questa spiegazione usa la gerarchia di classi di tipi
class Functor f
class Functor m => Monad m
In Haskell, l'attuale gerarchia standard è
class Functor f
class Functor p => Applicative p
class Applicative m => Monad m
perché non solo ogni monade è un funzione, ma ogni applicativo è un funzione e ogni monade è anche un applicativo.
Usando la monade elenco, lo pseudocodice imperativo
for a in (1, ..., 10)
for b in (1, ..., 10)
p <- a * b
if even(p)
yield p
si traduce approssimativamente nel blocco do ,
do a <- [1 .. 10]
b <- [1 .. 10]
let p = a * b
guard (even p)
return p
la comprensione della monade equivalente ,
[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]
e l'espressione
[1 .. 10] >>= (\ a ->
[1 .. 10] >>= (\ b ->
let p = a * b in
guard (even p) >> -- [ () | even p ] >>
return p
)
)
La notazione e la comprensione delle monadi sono zucchero sintattico per le espressioni di legame annidate. L'operatore bind viene utilizzato per l'associazione locale dei risultati monadici.
let x = v in e = (\ x -> e) $ v = v & (\ x -> e)
do { r <- m; c } = (\ r -> c) =<< m = m >>= (\ r -> c)
dove
(&) :: a -> (a -> b) -> b
(&) = flip ($)
infixl 0 &
La funzione di guardia è definita
guard :: Additive m => Bool -> m ()
guard True = return ()
guard False = fail
dove il tipo di unità o "tupla vuota"
data () = ()
Le monadi additive che supportano la scelta e il fallimento possono essere astratte usando una classe di tipo
class Monad m => Additive m where
fail :: m t
(<|>) :: m t -> m t -> m t
infixl 3 <|>
instance Additive Maybe where
fail = Nothing
Nothing <|> m = m
m <|> _ = m
instance Additive [] where
fail = []
(<|>) = (++)
dove fail
e <|>
formare un monoideforall k l m.
k <|> fail = k
fail <|> l = l
(k <|> l) <|> m = k <|> (l <|> m)
ed fail
è l'elemento zero assorbente / annichilente delle monadi additive
_ =<< fail = fail
Se dentro
guard (even p) >> return p
even p
è vero, quindi la guardia produce [()]
e, per definizione >>
, la funzione costante locale
\ _ -> return p
viene applicato al risultato ()
. Se falso, la guardia produce la lista monad's fail
( []
), che non produce alcun risultato per l'applicazione di una freccia Kleisli >>
, quindi questa p
viene saltata.
Stato
Stranamente, le monadi sono usate per codificare il calcolo con stato.
Un processore di stato è una funzione
forall st t. st -> (t, st)
che transita uno stato st
e produce un risultato t
. Lo stato st
può essere qualsiasi cosa. Niente, bandiera, conteggio, matrice, maniglia, macchina, mondo.
Il tipo di processori di stato viene solitamente chiamato
type State st t = st -> (t, st)
La monade processore stato è la kinded * -> *
functor State st
. Le frecce Kleisli della monade del processore di stato sono funzioni
forall st a b. a -> (State st) b
In canonico Haskell viene definita la versione pigra della monade del processore di stato
newtype State st t = State { stateProc :: st -> (t, st) }
instance Functor (State st) where
map :: (a -> b) -> ((State st) a -> (State st) b)
map f (State p) = State $ \ s0 -> let (x, s1) = p s0
in (f x, s1)
instance Monad (State st) where
return :: t -> (State st) t
return x = State $ \ s -> (x, s)
(=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
in stateProc (f x) s1
Un processore di stato viene eseguito fornendo uno stato iniziale:
run :: State st t -> st -> (t, st)
run = stateProc
eval :: State st t -> st -> t
eval = fst . run
exec :: State st t -> st -> st
exec = snd . run
L'accesso allo stato è fornito da primitivi get
e put
, metodi di astrazione su monadi statali :
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Monad m => Stateful m st | m -> st where
get :: m st
put :: st -> m ()
m -> st
dichiara una dipendenza funzionale del tipo di stato st
sulla monade m
; che a State t
, ad esempio, determinerà che il tipo di stato sia t
univoco.
instance Stateful (State st) st where
get :: State st st
get = State $ \ s -> (s, s)
put :: st -> State st ()
put s = State $ \ _ -> ((), s)
con il tipo di unità usato in modo analogo a void
in C.
modify :: Stateful m st => (st -> st) -> m ()
modify f = do
s <- get
put (f s)
gets :: Stateful m st => (st -> t) -> m t
gets f = do
s <- get
return (f s)
gets
viene spesso utilizzato con gli accessi ai campi record.
L'equivalente monade statale del threading variabile
let s0 = 34
s1 = (+ 1) s0
n = (* 12) s1
s2 = (+ 7) s1
in (show n, s2)
dove s0 :: Int
, è ugualmente referenzialmente trasparente, ma infinitamente più elegante e pratico
(flip run) 34
(do
modify (+ 1)
n <- gets (* 12)
modify (+ 7)
return (show n)
)
modify (+ 1)
è un calcolo di tipo State Int ()
, ad eccezione del suo effetto equivalente a return ()
.
(flip run) 34
(modify (+ 1) >>
gets (* 12) >>= (\ n ->
modify (+ 7) >>
return (show n)
)
)
La legge monade dell'associatività può essere scritta in termini di >>=
forall m f g.
(m >>= f) >>= g = m >>= (\ x -> f x >>= g)
o
do { do { do {
r1 <- do { x <- m; r0 <- m;
r0 <- m; = do { = r1 <- f r0;
f r0 r1 <- f x; g r1
}; g r1 }
g r1 }
} }
Come nella programmazione orientata all'espressione (ad esempio Rust), l'ultima istruzione di un blocco rappresenta il suo rendimento. L'operatore di bind viene talvolta chiamato "punto e virgola programmabile".
Le primitive della struttura di controllo dell'iterazione dalla programmazione imperativa strutturata sono emulate monadicamente
for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())
while :: Monad m => m Bool -> m t -> m ()
while c m = do
b <- c
if b then m >> while c m
else return ()
forever :: Monad m => m t
forever m = m >> forever m
Input Output
data World
La monade del processore di stato del mondo I / O è una riconciliazione di Haskell puro e del mondo reale, della semantica operativa denotativa e imperativa funzionale. Un analogo stretto dell'attuazione rigorosa effettiva:
type IO t = World -> (t, World)
L'interazione è facilitata da primitivi impuri
getChar :: IO Char
putChar :: Char -> IO ()
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
hSetBuffering :: Handle -> BufferMode -> IO ()
hTell :: Handle -> IO Integer
. . . . . .
L'impurità del codice che utilizza le IO
primitive è stabilita in modo permanente dal sistema dei tipi. Perché la purezza è fantastica, ciò che accade dentro IO
rimane dentro IO
.
unsafePerformIO :: IO t -> t
O, almeno, dovrebbe.
La firma del tipo di un programma Haskell
main :: IO ()
main = putStrLn "Hello, World!"
si espande a
World -> ((), World)
Una funzione che trasforma un mondo.
Epilogo
La categoria quali oggetti sono tipi di Haskell e quali morfismi sono funzioni tra i tipi di Haskell è, "veloce e sciolto", la categoria Hask
.
Un functor T
è una mappatura da una categoria C
a una categoria D
; per ogni oggetto in C
un oggetto inD
Tobj : Obj(C) -> Obj(D)
f :: * -> *
e per ogni morfismo in C
un morfismo inD
Tmor : HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
map :: (a -> b) -> (f a -> f b)
dove X
, Y
sono oggetti C
. HomC(X, Y)
è la classe di omomorfismo di tutti i morfismi X -> Y
in C
. Il funzionario deve preservare l'identità e la composizione del morfismo, la "struttura" di C
, in D
.
Tmor Tobj
T(id) = id : T(X) -> T(X) Identity
T(f) . T(g) = T(f . g) : T(X) -> T(Z) Composition
La categoria Kleisli di una categoria C
è data da una tripla Kleisli
<T, eta, _*>
di un endofunctor
T : C -> C
( f
), un morfismo di identità eta
( return
) e un operatore di estensione *
(=<<
).
Ogni morfismo Kleisli in Hask
f : X -> T(Y)
f :: a -> m b
dall'operatore di estensione
(_)* : Hom(X, T(Y)) -> Hom(T(X), T(Y))
(=<<) :: (a -> m b) -> (m a -> m b)
viene dato un morfismo nella Hask
categoria Kleisli
f* : T(X) -> T(Y)
(f =<<) :: m a -> m b
La composizione nella categoria Kleisli .T
è data in termini di estensione
f .T g = f* . g : X -> T(Z)
f <=< g = (f =<<) . g :: a -> m c
e soddisfa gli assiomi di categoria
eta .T g = g : Y -> T(Z) Left identity
return <=< g = g :: b -> m c
f .T eta = f : Z -> T(U) Right identity
f <=< return = f :: c -> m d
(f .T g) .T h = f .T (g .T h) : X -> T(U) Associativity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d
quale, applicando le trasformazioni di equivalenza
eta .T g = g
eta* . g = g By definition of .T
eta* . g = id . g forall f. id . f = f
eta* = id forall f g h. f . h = g . h ==> f = g
(f .T g) .T h = f .T (g .T h)
(f* . g)* . h = f* . (g* . h) By definition of .T
(f* . g)* . h = f* . g* . h . is associative
(f* . g)* = f* . g* forall f g h. f . h = g . h ==> f = g
in termini di estensione sono dati canonicamente
eta* = id : T(X) -> T(X) Left identity
(return =<<) = id :: m t -> m t
f* . eta = f : Z -> T(U) Right identity
(f =<<) . return = f :: c -> m d
(f* . g)* = f* . g* : T(X) -> T(Z) Associativity
(((f =<<) . g) =<<) = (f =<<) . (g =<<) :: m a -> m c
Le monadi possono anche essere definite in termini non di estensione kleisliana, ma di una naturale trasformazione mu
, in programmazione chiamata join
. Una monade è definita in termini di mu
tripla su una categoria C
, di un endofunctor
T : C -> C
f :: * -> *
e due trasformazioni naturali
eta : Id -> T
return :: t -> f t
mu : T . T -> T
join :: f (f t) -> f t
soddisfacendo le equivalenze
mu . T(mu) = mu . mu : T . T . T -> T . T Associativity
join . map join = join . join :: f (f (f t)) -> f t
mu . T(eta) = mu . eta = id : T -> T Identity
join . map return = join . return = id :: f t -> f t
Viene quindi definita la classe del tipo di monade
class Functor m => Monad m where
return :: t -> m t
join :: m (m t) -> m t
L' mu
implementazione canonica dell'opzione monade:
instance Monad Maybe where
return = Just
join (Just m) = m
join Nothing = Nothing
La concat
funzione
concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat [] = []
è la join
monade della lista.
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> ([a] -> [b])
(f =<<) = concat . map f
Le implementazioni di join
possono essere tradotte dal modulo di estensione usando l'equivalenza
mu = id* : T . T -> T
join = (id =<<) :: m (m t) -> m t
La traduzione inversa da mu
al modulo di estensione è data da
f* = mu . T(f) : T(X) -> T(Y)
(f =<<) = join . map f :: m a -> m b
Ma perché una teoria così astratta dovrebbe essere di qualche utilità per la programmazione?
La risposta è semplice: come scienziati informatici, apprezziamo l'astrazione ! Quando progettiamo l'interfaccia per un componente software, vogliamo che riveli il meno possibile sull'implementazione. Vogliamo essere in grado di sostituire l'implementazione con molte alternative, molte altre "istanze" dello stesso "concetto". Quando progettiamo un'interfaccia generica per molte librerie di programmi, è ancora più importante che l'interfaccia che scegliamo abbia una varietà di implementazioni. È la generalità del concetto di monade che apprezziamo così tanto, è perché teoria delle categorie è così astratta che i suoi concetti sono così utili per la programmazione.
Non sorprende, quindi, che la generalizzazione delle monadi che presentiamo di seguito abbia anche una stretta connessione con la teoria delle categorie. Ma sottolineiamo che il nostro scopo è molto pratico: non è "implementare la teoria delle categorie", è trovare un modo più generale di strutturare le librerie combinatrici. È semplicemente una nostra fortuna che i matematici abbiano già svolto gran parte del lavoro per noi!
da Generalising Monads a Arrows di John Hughes