Mentre cercavo di comprendere appieno le Monads mi sono imbattuto in Functors, Applicatives, And Monads In Pictures di Adit Bhargava che è stato di grande aiuto. Inoltre ho scoperto che il post è stato tradotto in francese, tedesco e anche in spagnolo, ma non in italiano. Poiché non è ammissibile essere inferiori ai nostri cugini d’oltralpe ho deciso di realizzare una traduzione in italiano.
Questo è un semplice valore:
Noi sappiamo applicare ad esso una funzione:
Fin qui tutto semplice. Adesso ipotizziamo che ogni valore possa trovarsi in un contesto. Per adesso possiamo pensare ad un contesto come ad una scatola in cui è possibile inserire un valore:
Quando andiamo ad applicare una funzione a questo valore, avremo risultati differenti in base al contesto. Questa è l’idea alla base dei Functors, Applicatives e delle Monads.
Il data type Maybe
definisce due contesti:
data Maybe a = Nothing | Just a
Tra qualche istante vedremo come cambia l’applicazione di una funzione quando qualcosa è un Just a
rispetto a quando è Nothing
, ma prima concentriamoci sui Functors!
Functors
Quando un valore è contenuto in un contesto non possiamo applicare una normale funzione ad esso:
Per risolvere questo problema utilizziamo la funzione fmap
.
fmap
sa come applicare le funzioni a valori che sono contenuti in un contesto.
Per esempio se volgiamo applicare (+3)
a Just 2
possiamo usare fmap
in questo modo:
> fmap (+3) (Just 2)
Just 5
Bam! fmap
ci mostra come si fa! Ma come fa fmap
a sapere come applicare la funzione?
Cos’è realmente un Functor?
Functor
è una typeclass la cui definizione è riportata di seguito:
Un Functor
è un qualsiasi data type che specifica come si applica ad esso la fmap
.
Ecco come funziona la fmap
:
Quindi è possibile fare questo:
> fmap (+3) (Just 2)
Just 5
Poiché Maybe
è un Functor
e quindi specifica come applicare la fmap
a Just
e a Nothing
:
instance Functor Maybe where
fmap func (Just val) = Just (func val)
fmap func Nothing = Nothing
Di seguito possiamo vedere cosa accade dietro le quinte quando scriviamo fmap (+3) (Just 2)
:
Fin qui tutto bene, ma se chiedessimo ad fmap
di applicare (+3)
a Nothing
cosa succederebbe?
> fmap (+3) Nothing
Nothing
Come Morpheus in Matrix, fmap
sa esattamente cosa fare; se si inizia con Nothing e si finisce con Nothing!
fmap
è zen.
Ora dovrebbe essere chiaro il perché esiste il data type Maybe
.
Ipotizziamo ora di dover estrarre un record da un database.
In un linguaggio senza Maybe
avremo qualcosa di simile:
post = Post.find_by_id(1)
if post
return post.title
else
return nil
end
Mentre in Haskell possiamo usare la fmap
per gestire la possibile assenza del post richiesto:
fmap (getPostTitle) (findPost 1)
Se findPost
restituisce un post, noi otterremo il titolo con getPostTitle
.
Se invece restituisce Nothing
, otteniamo Nothing
!
Piuttosto ordinato, vero?
<$>
è la versione infix della fmap
, per questo motivo può capitare spesso di vedere qualcosa del genere:
getPostTitle <$> (findPost 1)
Ora passiamo ad un altro esempio; cosa succede quando applichiamo una funzione ad una lista?
Anche le liste sono dei Functors! Di seguito è riportata la sua definizione:
instance Functor [] where
fmap = map
Un ultimo esempio prima di chiudere la sezione sui Functors. Cosa succede quando applichiamo una funzione ad un’altra funzione?
fmap (+3) (-1)
Innanzitutto concentriamoci sulla singola funzione:
E poi passiamo ad una funzione applicata ad un altra funzione:
Il risultato è un’altra funzione!
> import Control.Applicative
> let foo = fmap (+3) (+2)
> foo 10
15
Questo vuol dire che anche le funzioni sono dei Functors!
instance Functor ((->) r) where
fmap f g = f . g
Quindi quando utilizziamo la fmap
su una funzione stiamo eseguendo una composizione di funzioni.
Applicatives
Le Applicatives portano il tutto ad un altro livello. Con un’Applicative il nostro valore è contenuto in un contesto esattamente come con i Functor:
Ma adesso anche le funzioni possono essere racchiuse in un contesto!
Il modulo Control.Applicative
definisce l’operatore <*>
che sa come applicare delle funzioni contenute in un contesto ad un valore contenuto in un contesto.
L’uso di <*>
può far nascere delle situazioni interessanti come questa:
> [(*2), (+3)] <*> [1, 2, 3]
[2, 4, 6, 4, 5, 6]
Ora passiamo a qualcosa che può essere fatta con le Applicatives ma non con i Functors. Come possiamo applicare una funzione con due argomenti a due valori contenuti in un contesto?
Ecco cosa succede con i Functors:
> (+) <$> (Just 5)
Just (+5)
> Just (+5) <$> (Just 4)
ERROR ??? WHAT DOES THIS EVEN MEAN WHY IS THE FUNCTION WRAPPED IN A JUST
Mentre con le Applicatives:
> (+) <$> (Just 5)
Just (+5)
> Just (+5) <*> (Just 3)
Just 8
Le Applicatives fanno un po’ i bulli con i Functors.
“I grandi possono usare delle funzioni con un qualsiasi numero di argomenti”, dice l’Applicative.
“Armato di <$>
e <*>
, posso prendere una funzione che si aspetta un qualsiasi numero di argomenti non inseriti in un contesto e poi passarle tutti valori contenuti in un contesto ottenendo un nuovo valore con contesto! AHAHAHAHAHAH!”
> (*) <$> Just 5 <*> Just 3
Just 15
Abbiamo anche una scorciatoia, chiamata liftA2
, che ci permette di fare la stessa cosa:
> liftA2 (*) (Just 5) (Just 3)
Just 15
Monads
Come imparare le Monads:
- Conseguire una laurea in computer science.
- Metterla da parte poiché non è necessaria per questa sezione!
Le Monads alzano ulteriormente la posta in gioco. I Functors ci permettono di applicare funzioni a valori inclusi in un contesto:
Le Applicatives ci consentono di applicare funzioni con contesto a valori con contesto:
Le Monads ci permettono di applicare funzioni che ritornano un valore con contesto a dei valori contenuti in un contesto.
Le Monads hanno una funzione >>=
(letto “bind”) che ci permette di eseguire l’operazione precedentemente descritta.
Facciamo un esempio utilizzando Maybe
che oltre ad essere un Functor e un’Applicative è anche una Monad:
Immaginiamo di avere una funzione half
che può operare soltanto sui numeri pari:
half x = if even x
then Just (x `div` 2)
else Nothing
Cosa succede se le passiamo un valore contenuto in un contesto?
Possiamo usare l’operatore >>=
che ci consente di spingere il valore contenuto nel contesto all’interno della funzione.
Di seguito è riportata una foto dell’operatore bind:
Mentre qui possiamo vederlo all’opera:
> Just 3 >>= half
Nothing
> Just 4 >>= half
Just 2
> Nothing >>= half
Nothing
Cosa succede dietro le quinte?
Monad
è un’altra typeclass e questa è la sua definizione (parziale):
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
Ed ecco una vista di dettaglio sull’operatore >>=
:
Quindi Maybe
è una Monad:
instance Monad Maybe where
Nothing >>= func = Nothing
Just val >>= func = func val
Di seguito è riportata l’esecuzione della funzione half
dandole in pasto Just 3
!
Mentre dandole in ingresso Nothing
otteniamo:
Attraverso l’operatore >>=
possiamo anche concatenare diverse chiamate a funzione:
> Just 20 >>= half >>= half >>= half
Nothing
Tutto molto bello!
A questo punto noi sappiamo che Maybe
è un Functor
, una Applicatice
e una Monad
.
IO
Bene, ora passiamo ad un altro esempio, la Monad IO
:
E in particolare ci concentriamo su tre funzioni.
getLine
non necessita di argomenti e restituisce l’input dell’utente:
getLine :: IO String
readFile
prende in ingresso una stringa (il nome del file) e restituisce il contenuto del file:
readFile :: FilePath -> IO String
E infine putStrLn
che accetta in ingresso una stringa e la stampa:
putStrLn :: String -> IO ()
Tutte e tre queste funzioni hanno come input un valore semplice (o nessun valore) e restituiscono un valore contenuto nel contesto IO
.
Possiamo concatenare queste funzioni utilizzando >>=
!
getLine >>= readFile >>= putStrLn
Haskell mette a disposizione anche un po’ di syntactical sugar per operare con le Monads.
Infatti possiamo ottenere lo stesso risultato utilizando la do
notation:
foo = do
filename <- getLine
contents <- readFile filename
putStrLn contents
Adesso sei pronto per lo spettacolo delle Monads!
Conclusioni
- Un Functor è un data type che implementa la typeclass
Functor
. - Un’Applicative è un data type che implementa la typeclass
Applicative
. - Una Monad è un data type che implementa la typeclass
Monad
. - Il data type
Maybe
implementa tutte e tre le typeclass quindi è un Functor, un’Applicative e una Monad.
Qual è la differenza tra i tre?
Functor
: puoi applicare una funzione ad un valore contenuto in un contesto utilizzandofmap
o<$>
.Applicative
: puoi applicare una funzione con contesto a dei valori con contesto tramite<*>
oliftA
.Monad
: puoi applicare funzioni che ritornano un valore con contesto a valori contenuti in un contesto attraverso>>=
oliftM
.
Quindi, amico caro (penso che a questo punto possiamo considerarci amici), penso che siamo entrambi d’accordo che le Monads sono facili e SMART. Ora che hai avuto un assaggio, perché non fare come Mel Gibson e prendere l’intera bottiglia. Puoi iniziare ad approfondire il discorso sulle Monads nella sezione dedicata alle Monads di LYAH. Ci sono un sacco di cose su cui ho sorvolato perché Miran fa un ottimo lavoro nell’approfondire queste cose.