Per prima cosa distinguiamo tra l'apprendimento dei concetti astratti e l'apprendimento di esempi specifici di essi.
Non hai intenzione di andare molto lontano ignorando tutti gli esempi specifici, per la semplice ragione che sono assolutamente onnipresenti. Infatti, le astrazioni esistono in gran parte perché unificano le cose che faresti comunque con gli esempi specifici.
Le astrazioni stesse, d'altra parte, sono certamente utili , ma non sono immediatamente necessarie. Puoi ottenere abbastanza lontano ignorando completamente le astrazioni e semplicemente usando i vari tipi direttamente. Alla fine vorrai capirli, ma puoi sempre tornare indietro più tardi. In effetti, posso quasi garantire che, se lo fai, quando torni indietro ti schiaffi la fronte e ti chiedi perché hai passato tutto quel tempo a fare le cose nel modo più duro invece di usare i comodi strumenti generali.
Prendi Maybe a
come esempio. È solo un tipo di dati:
data Maybe a = Just a | Nothing
È tutto fuorché autodocumentante; è un valore facoltativo. O hai "solo" qualcosa di tipo a
, o non hai nulla. Supponiamo che tu abbia una funzione di ricerca di qualche tipo, che restituisca Maybe String
per rappresentare la ricerca di un valore String
che potrebbe non essere presente. In questo modo, il modello corrisponde al valore per vedere quale è:
case lookupFunc key of
Just val -> ...
Nothing -> ...
Questo è tutto!
In realtà, non c'è nient'altro di cui hai bisogno. Nessun Functor
s o Monad
s o altro. Quelli esprimono modi comuni di utilizzare valori di Maybe a
... ma sono solo espressioni idiomatiche, "schemi di progettazione", qualsiasi cosa tu voglia chiamare.
L'unico posto in cui non puoi davvero evitarlo completamente è con IO
, ma è comunque una misteriosa scatola nera, quindi non vale la pena provare a capire cosa significa Monad
o altro.
In effetti, ecco un cheat sheet per tutti voi veramente che devono sapere su IO
per ora:
-
Se qualcosa ha un tipo IO a
, significa che è una procedura che fa qualcosa e sputa un valore a
.
-
Quando hai un blocco di codice usando la notazione do
, scrivi qualcosa del tipo:
do -- ...
inp <- getLine
-- etc...
... significa eseguire la procedura alla destra di <-
, e assegnare il risultato al nome sulla sinistra.
-
Se invece hai qualcosa di simile:
do -- ...
let x = [foo, bar]
-- etc...
... significa assegnare il valore dell'espressione semplice (non una procedura) alla destra di =
al nome sulla sinistra.
-
Se metti qualcosa lì senza assegnare un valore, come questo:
do putStrLn "blah blah, fishcakes"
... significa eseguire una procedura e ignorare qualsiasi cosa ritorni. Alcune procedure hanno il tipo IO ()
- il tipo ()
è una specie di segnaposto che non dice nulla, quindi significa solo che la procedura fa qualcosa e non restituisce un valore. Un po 'come una funzione void
in altre lingue.
-
L'esecuzione della stessa procedura più di una volta può dare risultati diversi; è una specie di idea. Questo è il motivo per cui non c'è modo di "rimuovere" IO
da un valore, perché qualcosa in IO
non è un valore, è una procedura per ottenere un valore.
-
L'ultima riga in un blocco do
deve essere una semplice procedura senza assegnazione, in cui il valore di ritorno di tale procedura diventa il valore di ritorno per l'intero blocco. Se si desidera che il valore di ritorno utilizzi un valore già assegnato, la funzione return
assume un valore normale e fornisce una procedura non operativa che restituisce tale valore.
-
Oltre a questo, non c'è niente di speciale su IO
; queste procedure sono in realtà semplici valori, e puoi passarle intorno e combinarle in diversi modi. È solo quando vengono eseguiti in un blocco do
chiamato da qualche parte da main
che fanno qualsiasi cosa.
Quindi, in qualcosa come questo programma di esempio stereotipato e noiosissimo:
hello = do putStrLn "What's your name?"
name <- getLine
let msg = "Hi, " ++ name ++ "!"
putStrLn msg
return name
... puoi leggerlo come un programma imperativo. Stiamo definendo una procedura denominata hello
. Quando viene eseguito, prima esegue una procedura per stampare un messaggio che chiede il tuo nome; successivamente esegue una procedura che legge una riga di input e assegna il risultato a name
; quindi assegna un'espressione al nome msg
; poi stampa il messaggio; quindi restituisce il nome dell'utente come risultato dell'intero blocco. Poiché name
è un String
, ciò significa che hello
è una procedura che restituisce un String
, quindi ha tipo IO String
. E ora puoi eseguire questa procedura altrove, proprio come si esegue getLine
.
Pfff, monadi. Chi ne ha bisogno?