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:

Due

Noi sappiamo applicare ad esso una funzione:

Una somma

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:

Due all’interno di un contesto

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:

I contesti di Maybe

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:

Ouch

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

Applicazione di una funzione con fmap

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:

Definizione della typeclass FUnctor

Un Functor è un qualsiasi data type che specifica come si applica ad esso la fmap. Ecco come funziona la fmap:

Definizione della funzione 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):

Applicazione di fmap a Just

Fin qui tutto bene, ma se chiedessimo ad fmap di applicare (+3) a Nothing cosa succederebbe?

Applicazione di fmap a Nothing

> fmap (+3) Nothing
Nothing

Applicazione di fmap a 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?

Applicazione di fmap 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:

Esempio di una funzione

E poi passiamo ad una funzione applicata ad un altra funzione:

Applicazione di fmap ad una 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:

Due all&rsquo;interno di un contesto

Ma adesso anche le funzioni possono essere racchiuse in un contesto!

Una funzione 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.

Uso di un&rsquo;Applicative con Just

L’uso di <*> può far nascere delle situazioni interessanti come questa:

> [(*2), (+3)] <*> [1, 2, 3]
[2, 4, 6, 4, 5, 6]

Uso di un&rsquo;Applicative su una lista

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:

  1. Conseguire una laurea in computer science.
  2. 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:

Esempio di un Functor

Le Applicatives ci consentono di applicare funzioni con contesto a valori con contesto:

Esempio di un&rsquo;Applicative

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:

Esempio di contesto

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

Funzione half

Cosa succede se le passiamo un valore contenuto in un contesto?

Funzione half ouch

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:

Foto dell&rsquo;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 >>=:

Operatore bind

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!

Esempio di monad con Just

Mentre dandole in ingresso Nothing otteniamo:

Esempio di monad con Nothing

Attraverso l’operatore >>= possiamo anche concatenare diverse chiamate a funzione:

> Just 20 >>= half >>= half >>= half
Nothing

Esempio di una catena di Monads

Whoa!

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:

IO Monad

E in particolare ci concentriamo su tre funzioni. getLine non necessita di argomenti e restituisce l’input dell’utente:

getLine

getLine :: IO String

readFile prende in ingresso una stringa (il nome del file) e restituisce il contenuto del file:

readFile

readFile :: FilePath -> IO String

E infine putStrLn che accetta in ingresso una stringa e la stampa:

putStrLn

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 >>=!

Concatenazione della Monad IO

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

  1. Un Functor è un data type che implementa la typeclass Functor.
  2. Un’Applicative è un data type che implementa la typeclass Applicative.
  3. Una Monad è un data type che implementa la typeclass Monad.
  4. Il data type Maybe implementa tutte e tre le typeclass quindi è un Functor, un’Applicative e una Monad.

Qual è la differenza tra i tre?

Differenza tra Functor, Applicative e Monad

  • Functor: puoi applicare una funzione ad un valore contenuto in un contesto utilizzando fmap o <$>.
  • Applicative: puoi applicare una funzione con contesto a dei valori con contesto tramite <*> o liftA.
  • Monad: puoi applicare funzioni che ritornano un valore con contesto a valori contenuti in un contesto attraverso >>= o liftM.

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.