tutorial online funciones empresa descargar constructora company haskell

online - ¿Debería evitarse la notación en Haskell?



haskell pdf (7)

La mayoría de los tutoriales de Haskell enseñan el uso de la notación do para IO.

También comencé con la notación do, pero eso hace que mi código parezca más un lenguaje imperativo que un lenguaje FP.

Esta semana vi un tutorial usar IO con <$>

stringAnalyzer <$> readFile "testfile.txt"

en lugar de usar do

main = do strFile <- readFile "testfile.txt" let analysisResult = stringAnalyzer strFile return analysisResult

Y la herramienta de análisis de registro está terminada sin el do .

Entonces mi pregunta es " ¿Deberíamos evitar la notación do en cualquier caso? ".

Sé que tal vez hará que el código sea mejor en algunos casos.

Además, ¿por qué la mayoría de los tutoriales enseñan IO con do ?

En mi opinión, <$> y <*> hacen que el código sea más FP que IO.


¿Deberíamos evitar la notación do en cualquier caso?

Yo diría definitivamente que no . Para mí, el criterio más importante en tales casos es hacer que el código sea lo más legible y comprensible posible . La " do notación" se introdujo para hacer que el código monádico sea más comprensible, y esto es lo que importa. Claro, en muchos casos, usar la notación Point-Free aplicable es muy agradable, por ejemplo, en lugar de

do f <- [(+1), (*7)] i <- [1..5] return $ f i

escribiríamos solo [(+1), (*7)] <*> [1..5] .

Pero hay muchos ejemplos en los que no usar la función do -notation hará que el código sea muy ilegible. Considera este ejemplo :

nameDo :: IO () nameDo = do putStr "What is your first name? " first <- getLine putStr "And your last name? " last <- getLine let full = first++" "++last putStrLn ("Pleased to meet you, "++full++"!")

aquí está bastante claro qué está sucediendo y cómo se secuencian las acciones de IO . Una notación do free se parece a

name :: IO () name = putStr "What is your first name? " >> getLine >>= f where f first = putStr "And your last name? " >> getLine >>= g where g last = putStrLn ("Pleased to meet you, "++full++"!") where full = first++" "++last

o como

nameLambda :: IO () nameLambda = putStr "What is your first name? " >> getLine >>= /first -> putStr "And your last name? " >> getLine >>= /last -> let full = first++" "++last in putStrLn ("Pleased to meet you, "++full++"!")

que son mucho menos legibles Ciertamente, aquí la nota de do es mucho más preferible aquí.

Si desea evitar el uso de do , intente estructurar su código en muchas funciones pequeñas. Este es un buen hábito de todos modos, y puede reducir su bloque do para contener solo 2-3 líneas, que luego pueden ser reemplazadas por >>= , <$>, <*> `etc. Por ejemplo, lo anterior podría ser reescrito como

name = getName >>= welcome where ask :: String -> IO String ask s = putStr s >> getLine join :: [String] -> String join = concat . intersperse " " getName :: IO String getName = join <$> traverse ask ["What is your first name? ", "And your last name? "] welcome :: String -> IO () welcome full = putStrLn ("Pleased to meet you, "++full++"!")

Es un poco más largo, y tal vez un poco menos comprensible para los principiantes de Haskell (debido a intersperse , concat y traverse ), pero en muchos casos esas funciones nuevas y pequeñas pueden reutilizarse en otros lugares de su código, lo que lo hará más estructurado y composable

Yo diría que la situación es muy similar a si usar la notación sin puntos o no. En muchos casos (como en el ejemplo más alto [(+1), (*7)] <*> [1..5] ) la notación sin puntos es excelente, pero si intenta convertir una expresión complicada , obtendrás resultados como

f = ((ite . (<= 1)) `flip` 1) <*> (((+) . (f . (subtract 1))) <*> (f . (subtract 2))) where ite e x y = if e then x else y

Me llevaría bastante tiempo entenderlo sin ejecutar el código. [Spoiler a continuación:]

fx = if (x <= 1) then 1 else f (x-1) + f (x-2)

Además, ¿por qué la mayoría de los tutoriales enseñan IO con do?

Porque IO está diseñado exactamente para imitar cálculos imperativos con efectos secundarios, por lo que su secuenciación con do es muy natural.


En mi opinión, <$> y <*> hacen que el código sea más FP que IO.

Haskell no es un lenguaje puramente funcional porque "se ve mejor". A veces sucede, a menudo no es así. La razón para mantenerse funcional no es su sintaxis sino su semántica . Nos equipa con transparencia referencial, lo que hace que sea mucho más fácil probar invariantes, permite optimizaciones de muy alto nivel, hace que sea fácil escribir código de propósito general, etc.

Nada de esto tiene mucho que ver con la sintaxis. Los cómputos monádicos siguen siendo puramente funcionales, independientemente de si los escribes con notación do o con <$> , <*> y >>= , por lo que obtenemos los beneficios de Haskell de cualquier manera.

Sin embargo, a pesar de los beneficios de FP antes mencionados, a menudo es más intuitivo pensar en algoritmos desde un punto de vista imperativo, incluso si está acostumbrado a cómo esto se implementa a través de las mónadas. En estos casos, la notación do le da esta rápida percepción de "orden de cálculo", "origen de datos", "punto de modificación", sin embargo, es trivial desengrasarla manualmente en su cabeza a la >>= versión, para captar lo que es funcionando funcionalmente

El estilo aplicativo es ciertamente genial en muchos sentidos, sin embargo, es inherentemente sin puntos. Suele ser algo bueno, pero especialmente en problemas más complejos puede ser muy útil dar nombres a variables "temporales". Cuando se usa solo la sintaxis Haskell "FP", esto requiere lambdas o funciones explícitamente nombradas. Ambos tienen buenos casos de uso, pero el primero introduce bastante ruido justo en el medio de tu código y este último más bien interrumpe el "flujo", ya que requiere un where o let en otro lugar desde donde lo usas. do , por otro lado, le permite introducir una variable con nombre justo donde lo necesita, sin introducir ningún ruido en absoluto.


A menudo me encuentro primero escribiendo una acción monádica en notación de do , y luego refactorizandola a una expresión monádica (o funcionaria) simple. Esto sucede principalmente cuando el bloque do resulta ser más corto de lo que esperaba. A veces me refactorizo ​​en la dirección opuesta; depende del código en cuestión.

Mi regla general es: si el bloque do solo tiene un par de líneas, generalmente es más ordenado que una expresión corta. Un bloque de do largo es probablemente más legible tal como es, a menos que puedas encontrar una forma de dividirlo en funciones más pequeñas y más compostables.

Como un ejemplo trabajado, así es como podríamos transformar su fragmento de código detallado en uno simple.

main = do strFile <- readFile "testfile.txt" let analysisResult = stringAnalyzer strFile return analysisResult

En primer lugar, observe que las últimas dos líneas tienen la forma let x = y in return x . Esto, por supuesto, se puede transformar en simplemente return y .

main = do strFile <- readFile "testfile.txt" return (stringAnalyzer strFile)

Este es un bloque do muy corto: readFile "testfile.txt" a un nombre, y luego hacemos algo con ese nombre en la siguiente línea. Probemos ''eliminar azúcar'' como lo hará el compilador:

main = readFile "testFile.txt" >>= /strFile -> return (stringAnalyser strFile)

Mira la forma lambda en el lado derecho de >>= . Está pidiendo que se reescriba en estilo sin puntos: /x -> f $ gx convierte en /x -> (f . g) x que se convierte en f . g f . g .

main = readFile "testFile.txt" >>= (return . stringAnalyser)

Esto ya es mucho más ordenado que el bloque do original, pero podemos ir más allá.

Este es el único paso que requiere un poco de reflexión (aunque una vez que esté familiarizado con las mónadas y funtores, debería ser obvio). La función anterior es sugestiva de una de las leyes de mónadas : (m >>= return) == m . La única diferencia es que la función en el lado derecho de >>= no es solo el return : le hacemos algo al objeto dentro de la mónada antes de envolverlo en una return . Pero el patrón de ''hacer algo con un valor ajustado sin afectar su envoltorio'' es exactamente para lo que Functor es. Todas las mónadas son funtores, por lo que podemos refactorizar esto para que ni siquiera necesitemos la instancia de Monad :

main = fmap stringAnalyser (readFile "testFile.txt")

Finalmente, tenga en cuenta que <$> es solo otra forma de escribir fmap .

main = stringAnalyser <$> readFile "testFile.txt"

Creo que esta versión es mucho más clara que el código original. Se puede leer como una oración: " main es stringAnalyser aplicado al resultado de leer "testFile.txt" ". La versión original te aturde en los detalles de procedimiento de su operación.

Adición: mi comentario de que "todas las mónadas son funcionadoras" puede ser justificado por la observación de que m >>= (return . f) (también conocido como liftM la biblioteca estándar) es lo mismo que fmap fm . Si tienes una instancia de Monad , obtienes una instancia de Functor ''gratis'' - ¡simplemente define fmap = liftM ! Si alguien definió una instancia de Monad para su tipo pero no instancias para Functor y Applicative , lo llamaría un error. Los clientes esperan poder usar métodos Functor en instancias de Monad sin demasiada molestia.


El estilo aplicativo debe fomentarse porque se compone (y es más bonito). El estilo monádico es necesario en ciertos casos. Consulte https://.com/a/7042674/1019205 para obtener una explicación detallada.


La notación do se expande a una expresión usando las funciones (>>=) y (>>) y la expresión let . Por lo tanto, no es parte del núcleo del lenguaje.

(>>=) y (>>) se usan para combinar acciones secuencialmente y son esenciales cuando el resultado de una acción cambia la estructura de las siguientes acciones.

En el ejemplo dado en la pregunta, esto no es aparente, ya que solo hay una acción IO , por lo tanto, no es necesaria una secuencia.

Considera por ejemplo la expresión

do x <- getLine print (length x) y <- getLine return (x ++ y)

que se traduce a

getLine >>= /x -> print (length x) >> getLine >>= /y -> return (x ++ y)

En este ejemplo, se necesita la notación do (o las funciones (>>=) y (>>) para secuenciar las acciones IO .

Tan pronto o más tarde el programador lo necesitará.


do notación en Haskell desugars de una manera bastante simple.

do x <- foo e1 e2 ...

se convierte en

foo >>= /x -> do e1 e2

y

do x e1 e2 ...

dentro

x >> do e1 e2 ....

Esto significa que realmente puedes escribir cualquier cálculo monádico con >>= y return . La única razón por la que no lo hacemos es porque es una sintaxis más dolorosa. Las mónadas son útiles para imitar el código imperativo, la notación lo hace parecer así.

La sintaxis C-ish hace que sea mucho más fácil para los principiantes entenderlo. Tienes razón, no parece tan funcional, pero requerir a alguien que asimile las mónadas de manera adecuada antes de que pueda usar IO es una gran disuasión.

La razón por la que usaríamos >>= y return por otro lado es porque es mucho más compacta para 1 - 2 líneas. Sin embargo, tiende a ser un poco más ilegible para algo demasiado grande. Entonces, para responder directamente a su pregunta, por favor no evite hacer notación cuando sea apropiado.

Por último, los dos operadores que viste, <$> y <*> , son en realidad fmap y aplicativos respectivamente, no monádicos. En realidad, no se pueden usar para representar mucho de lo que hace la notación. Son más compactos, desde luego, pero no te permiten nombrar fácilmente valores intermedios. Personalmente, los utilizo cerca del 80% del tiempo, sobre todo porque tiendo a escribir funciones compostables muy pequeñas de todas formas para qué aplicaciones son ideales.


do notación es solo un azúcar sintáctico. Se puede evitar en todos los casos. Sin embargo, en algunos casos, reemplazar do con >>= y return hace que el código sea menos legible.

Entonces, para sus preguntas:

"¿Deberíamos evitar la declaración Do en cualquier caso?".

Concéntrate en hacer tu código más claro y mejor legible. Use do cuando lo ayude, evítelo de otra manera.

Y tenía otra pregunta, ¿por qué la mayoría de los tutoriales enseñarán IO con do?

Porque hace que el código IO sea mejor legible en muchos casos.

Además, la mayoría de las personas que comienzan a aprender Haskell tienen una experiencia de programación imperativa. Los tutoriales son para principiantes. Deben usar un estilo que sea fácil de entender para los recién llegados.