È passato un anno da quando ho pubblicato questa domanda. Dopo averlo pubblicato, ho approfondito Haskell per un paio di mesi. Mi è piaciuto moltissimo, ma l'ho messo da parte proprio mentre ero pronto a scavare nelle Monadi. Sono tornato al lavoro e mi sono concentrato sulle tecnologie richieste dal mio progetto.
Questo è abbastanza bello. È un po 'astratto però. Posso immaginare persone che non sanno quali monadi siano già confuse a causa della mancanza di esempi reali.
Quindi lasciami provare a rispettare, e solo per essere davvero chiaro farò un esempio in C #, anche se sembrerà brutto. Aggiungerò l'Haskell equivalente alla fine e ti mostrerò il fantastico zucchero sintattico di Haskell che è dove, IMO, le monadi iniziano davvero a diventare utili.
Ok, quindi una delle Monadi più semplici è chiamata "Forse monade" in Haskell. In C # viene chiamato il tipo Maybe Nullable<T>
. È fondamentalmente una piccola classe che incapsula semplicemente il concetto di un valore che è valido e ha un valore, oppure è "null" e non ha valore.
Una cosa utile da tenere dentro una monade per combinare valori di questo tipo è la nozione di fallimento. Vale a dire che vogliamo essere in grado di guardare più valori nullable e restituire null
non appena uno di essi è nullo. Questo potrebbe essere utile se, ad esempio, cerchi molte chiavi in un dizionario o qualcosa del genere e alla fine desideri elaborare tutti i risultati e combinarli in qualche modo, ma se una qualsiasi delle chiavi non è presente nel dizionario, vuoi tornare null
per tutto. Sarebbe noioso dover controllare manualmente ogni ricerca
null
e ritorno, quindi possiamo nascondere questo controllo all'interno dell'operatore di bind (che è una specie di punto di monade, nascondiamo la contabilità nell'operatore di bind che rende il codice più facile da utilizzare poiché possiamo dimenticare i dettagli).
Ecco il programma che motiva il tutto (lo definirò più
Bind
avanti, questo è solo per mostrarti perché è bello).
class Program
{
static Nullable<int> f(){ return 4; }
static Nullable<int> g(){ return 7; }
static Nullable<int> h(){ return 9; }
static void Main(string[] args)
{
Nullable<int> z =
f().Bind( fval =>
g().Bind( gval =>
h().Bind( hval =>
new Nullable<int>( fval + gval + hval ))));
Console.WriteLine(
"z = {0}", z.HasValue ? z.Value.ToString() : "null" );
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
Ora, ignora per un momento che esiste già il supporto per farlo Nullable
in C # (puoi aggiungere insieme null nullable e ottenere null se uno dei due è null). Facciamo finta che non ci sia una tale caratteristica, ed è solo una classe definita dall'utente senza magia speciale. Il punto è che possiamo usare la Bind
funzione per associare una variabile al contenuto del nostro Nullable
valore e quindi far finta che non ci sia nulla di strano e usarli come normali input e semplicemente sommarli. Abbiamo avvolgere il risultato in un annullabile alla fine, e che annullabile o sarà nullo (se qualsiasi f
, g
o h
ritorni null) o sarà il risultato della somma f
, g
eh
insieme. (questo è analogo al modo in cui possiamo associare una riga in un database a una variabile in LINQ e fare cose con essa, al sicuro nella consapevolezza che l' Bind
operatore si assicurerà che alla variabile vengano passati solo valori di riga validi).
Puoi giocare con questo e cambiare uno qualsiasi di f
, g
e h
per restituire null e vedrai che l'intera cosa restituirà null.
Quindi, chiaramente, l'operatore di bind deve fare questo controllo per noi, e salvarlo restituendo null se incontra un valore null, e altrimenti passa il valore all'interno della Nullable
struttura nella lambda.
Ecco l' Bind
operatore:
public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f )
where B : struct
where A : struct
{
return a.HasValue ? f(a.Value) : null;
}
I tipi qui sono proprio come nel video. Prende un M a
( Nullable<A>
nella sintassi C # per questo caso) e una funzione da a
a
M b
( Func<A, Nullable<B>>
nella sintassi C #) e restituisce un M b
( Nullable<B>
).
Il codice controlla semplicemente se il nullable contiene un valore e, in tal caso, lo estrae e lo passa alla funzione, altrimenti restituisce solo null. Ciò significa che l' Bind
operatore gestirà per noi tutta la logica di controllo null. Se e solo se il valore che chiamiamo
Bind
non è nullo, quel valore verrà "trasmesso" alla funzione lambda, altrimenti eseguiremo il salvataggio in anticipo e l'intera espressione sarà nulla. Ciò consente al codice che scriviamo utilizzando la monade di essere completamente libero da questo comportamento di controllo null, usiamo Bind
e otteniamo una variabile legata al valore all'interno del valore monadico ( e fval
,
nel codice di esempio) e possiamo usarli in modo sicuro nella conoscenza che si occuperà di controllarli per null prima di passarli.gval
hval
Bind
Ci sono altri esempi di cose che puoi fare con una monade. Ad esempio, è possibile fare in modo che l' Bind
operatore si occupi di un flusso di input di caratteri e utilizzarlo per scrivere combinatori parser. Ogni combinatore di parser può quindi essere completamente ignaro di cose come il back-tracking, i fallimenti del parser ecc. E combinare semplicemente parser più piccoli insieme come se le cose non andassero mai male, sicure nella consapevolezza che un'implementazione intelligente Bind
risolve tutta la logica dietro pezzi difficili. Quindi, in seguito, forse qualcuno aggiunge la registrazione alla monade, ma il codice che utilizza la monade non cambia, poiché tutta la magia si verifica nella definizione Bind
dell'operatore, il resto del codice rimane invariato.
Infine, ecco l'implementazione dello stesso codice in Haskell ( --
inizia una riga di commento).
-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a
-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x
-- the "unit", called "return"
return = Just
-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
g >>= ( \gval ->
h >>= ( \hval -> return (fval+gval+hval ) ) ) )
-- The following is exactly the same as the three lines above
z2 = do
fval <- f
gval <- g
hval <- h
return (fval+gval+hval)
Come puoi vedere la bella do
notazione alla fine fa sembrare un codice imperativo diretto. E infatti questo è di progettazione. Le monadi possono essere usate per incapsulare tutte le cose utili nella programmazione imperativa (stato mutabile, IO, ecc.) E usate usando questa bella sintassi simil-imperativa, ma dietro le tende ci sono solo monadi e un'implementazione intelligente dell'operatore bind! Il bello è che puoi implementare le tue monadi implementando >>=
e return
. E se lo fai, quelle monadi saranno anche in grado di usare la do
notazione, il che significa che puoi praticamente scrivere le tue piccole lingue semplicemente definendo due funzioni!