Primero distingamos entre aprender los conceptos abstractos y aprender ejemplos específicos de ellos.
No vas a llegar muy lejos ignorando todos los ejemplos específicos, por la simple razón de que son completamente ubicuos. De hecho, las abstracciones existen en gran parte porque unifican las cosas que estaría haciendo de todos modos con los ejemplos específicos.
Las abstracciones en sí mismas, por otro lado, son ciertamente útiles , pero no son inmediatamente necesarias. Puedes llegar bastante lejos ignorando por completo las abstracciones y simplemente usando los diferentes tipos directamente. Eventualmente querrás entenderlos, pero siempre puedes volver a ello más tarde. De hecho, casi puedo garantizar que si haces eso, cuando vuelvas a hacerlo, te darás una palmada en la frente y te preguntarás por qué pasaste todo ese tiempo haciendo las cosas de la manera más difícil en lugar de usar las prácticas herramientas de uso general.
Toma Maybe acomo ejemplo. Es solo un tipo de datos:
data Maybe a = Just a | Nothing
Es todo menos autodocumentado; Es un valor opcional. O tienes "solo" algo de tipo ao no tienes nada. Digamos que tiene una función de búsqueda de algún tipo, que vuelve Maybe Stringa representar la búsqueda de un Stringvalor que puede no estar presente. Entonces, el patrón coincide en el valor para ver cuál es:
case lookupFunc key of
Just val -> ...
Nothing -> ...
¡Eso es todo!
Realmente, no hay nada más que necesites. No Functorso Monads ni nada más. Esos expresan formas comunes de usar Maybe avalores ... pero son solo expresiones idiomáticas, "patrones de diseño", como quieran llamarlo.
El único lugar en el que realmente no puedes evitarlo es IO, pero de todos modos es una misteriosa caja negra, por lo que no vale la pena tratar de entender lo que significa como Monado lo que sea.
De hecho, aquí hay una hoja de trucos para todo lo que realmente necesita saber IOpor ahora:
Si algo tiene un tipo IO a, eso significa que es un procedimiento que hace algo y escupe un avalor.
Cuando tienes un bloque de código usando la donotación, escribe algo como esto:
do -- ...
inp <- getLine
-- etc...
... significa ejecutar el procedimiento a la derecha de <-, y asignar el resultado al nombre a la izquierda.
Mientras que si tienes algo como esto:
do -- ...
let x = [foo, bar]
-- etc...
... significa asignar el valor de la expresión simple (no un procedimiento) a la derecha del =nombre a la izquierda.
Si coloca algo allí sin asignar un valor, así:
do putStrLn "blah blah, fishcakes"
... significa ejecutar un procedimiento e ignorar todo lo que devuelve. Algunos procedimientos tienen el tipo IO ()--el ()tipo es una especie de marcador de posición que no dice nada, por lo que simplemente significa que el procedimiento hace algo y no devuelve un valor. Algo así como una voidfunción en otros idiomas.
Ejecutar el mismo procedimiento más de una vez puede dar resultados diferentes; Esa es la idea. Es por eso que no hay forma de "eliminar" el IOvalor de un valor, porque algo en IOno es un valor, es un procedimiento para obtener un valor.
La última línea en un dobloque debe ser un procedimiento simple sin asignación, donde el valor de retorno de ese procedimiento se convierte en el valor de retorno para todo el bloque. Si desea que el valor de retorno use algún valor ya asignado, la returnfunción toma un valor simple y le brinda un procedimiento sin operación que devuelve ese valor.
Aparte de eso, no tiene nada de especial IO; estos procedimientos son en realidad valores simples en sí mismos, y puede pasarlos y combinarlos de diferentes maneras. Es solo cuando se ejecutan en un dobloque llamado en algún lugar mainque hacen algo.
Entonces, en algo como este programa de ejemplo completamente aburrido y estereotipado:
hello = do putStrLn "What's your name?"
name <- getLine
let msg = "Hi, " ++ name ++ "!"
putStrLn msg
return name
... puedes leerlo como un programa imprescindible. Estamos definiendo un procedimiento llamado hello. Cuando se ejecuta, primero ejecuta un procedimiento para imprimir un mensaje preguntando su nombre; a continuación, ejecuta un procedimiento que lee una línea de entrada y asigna el resultado a name; entonces asigna una expresión al nombre msg; luego imprime el mensaje; luego devuelve el nombre del usuario como resultado de todo el bloque. Como namees a String, esto significa que helloes un procedimiento que devuelve a String, por lo que tiene tipo IO String. Y ahora puede ejecutar este procedimiento en otro lugar, tal como se ejecuta getLine.
Pfff, mónadas. ¿Quién los necesita?