¿Buen estilo de codificación Haskell del bloque de control if/else?
-
02-07-2019 - |
Pregunta
Estoy aprendiendo Haskell con la esperanza de que me ayude a acercarme a la programación funcional.Anteriormente, he usado principalmente lenguajes con sintaxis similar a C, como C, Java y D.
Tengo una pequeña pregunta sobre el estilo de codificación de un if
/else
bloque de control utilizado por el tutorial sobre wikilibros.El código se parece al siguiente:
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
if (read guess) < num
then do putStrLn "Too low!"
doGuessing num
else if (read guess) > num
then do putStrLn "Too high!"
doGuessing num
else do putStrLn "You Win!"
Me confunde, porque este estilo de codificación viola totalmente el estilo recomendado en lenguajes tipo C, donde debemos sangrar if
, else if
, y else
en la misma columna.
Sé que simplemente no funciona en Haskell, porque sería un error de análisis si aplicara sangría else
en la misma columna que if
.
Pero ¿qué pasa con el siguiente estilo?Creo que es mucho más claro que el anterior.Pero dado que Wikibooks y Yet Another Haskell Tutorial utilizan lo anterior, que está marcado como "mejor tutorial disponible en línea" en el sitio web oficial de Haskell, no estoy seguro de si este estilo de codificación es una convención en los programas de Haskell.
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
if (read guess) < num then
do
putStrLn "Too low!"
doGuessing num
else if (read guess) > num then do
putStrLn "Too high!"
doGuessing num
else do
putStrLn "You Win!"
Entonces, tengo curiosidad por saber qué estilo de codificación se usa con más frecuencia, ¿o hay otro estilo de codificación para este fragmento de código?
Solución
¡El estilo Haskell es funcional, no imperativo!En lugar de "hacer esto y luego aquello", piense en combinar funciones y describir qué su programa funcionará, no cómo.
En el juego, tu programa le pide al usuario que adivine.Una suposición correcta es ganadora.En caso contrario, el usuario vuelve a intentarlo.El juego continúa hasta que el usuario acierta, por lo que escribimos que:
main = untilM (isCorrect 42) (read `liftM` getLine)
Esto utiliza un combinador que ejecuta repetidamente una acción (getLine
extrae una línea de entrada y read
convierte esa cadena en un número entero en este caso) y verifica su resultado:
untilM :: Monad m => (a -> m Bool) -> m a -> m ()
untilM p a = do
x <- a
done <- p x
if done
then return ()
else untilM p a
El predicado (parcialmente aplicado en main
) compara la suposición con el valor correcto y responde en consecuencia:
isCorrect :: Int -> Int -> IO Bool
isCorrect num guess =
case compare num guess of
EQ -> putStrLn "You Win!" >> return True
LT -> putStrLn "Too high!" >> return False
GT -> putStrLn "Too low!" >> return False
La acción que se ejecutará hasta que el jugador adivine correctamente es
read `liftM` getLine
¿Por qué no mantenerlo simple y simplemente componer las dos funciones?
*Main> :type read . getLine <interactive>:1:7: Couldn't match expected type `a -> String' against inferred type `IO String' In the second argument of `(.)', namely `getLine' In the expression: read . getLine
El tipo de getLine
es IO String
, pero read
quiere un puro String
.
La función liftM
de Control.Monad toma una función pura y la "eleva" a una mónada.El tipo de expresión nos dice mucho sobre lo que hace:
*Main> :type read `liftM` getLine read `liftM` getLine :: (Read a) => IO a
Es una acción de E/S que al ejecutarla nos devuelve un valor convertido con read
, un Int
en nuestro caso.Recordar que readLine
es una acción de E/S que produce String
valores, para que puedas pensar en liftM
como permitiéndonos aplicar read
"dentro de IO
monada.
Juego de muestra:
1 Too low! 100 Too high! 42 You Win!
Otros consejos
Puedes usar la construcción "case":
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
case (read guess) of
g | g < num -> do
putStrLn "Too low!"
doGuessing num
g | g > num -> do
putStrLn "Too high!"
doGuessing num
otherwise -> do
putStrLn "You Win!"
Una mejora menor a la declaración de caso de Mattiast (lo editaría, pero me falta karma) es usar la función de comparación, que devuelve uno de tres valores, LT, GT o EQ:
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
case (read guess) `compare` num of
LT -> do putStrLn "Too low!"
doGuessing num
GT -> do putStrLn "Too high!"
doGuessing num
EQ -> putStrLn "You Win!"
Realmente me gustan estas preguntas de Haskell y animaría a otros a publicar más.A menudo sientes que hay consiguió Parece ser una mejor manera de expresar lo que estás pensando, pero Haskell es inicialmente tan extraño que no te viene nada a la mente.
Pregunta adicional para el periodista de Haskell:¿Cuál es el tipo de doGuessing?
La forma en que Haskell interpreta if ... then ... else
Dentro de un do
El bloque está muy en consonancia con toda la sintaxis de Haskell.
Pero mucha gente prefiere una sintaxis ligeramente diferente, lo que permite then
y else
aparecer en el mismo nivel de sangría que el correspondiente if
.Por lo tanto, GHC viene con una extensión de idioma opcional llamada DoAndIfThenElse
, que permite esta sintaxis.
El DoAndIfThenElse
La extensión se convierte en parte del lenguaje principal en la última revisión de la especificación Haskell. Haskel 2010.
Tenga en cuenta que muchos consideran que el hecho de tener que sangrar el 'entonces' y el 'si no' dentro de un bloque 'hacer' es un error.Probablemente se solucionará en Haskell' (Haskell prime), la próxima versión de la especificación de Haskell.
También puede utilizar agrupación explícita con llaves.Consulte la sección de diseño de http://www.haskell.org/tutorial/patterns.html
Aunque no lo recomendaría.Nunca he visto a nadie usar agrupaciones explícitas, excepto en algunos casos especiales.Normalmente miro el Código de preludio estándar para ejemplos de estilo.
Utilizo un estilo de codificación como tu ejemplo de Wikibooks.Claro, no sigue las pautas de C, pero Haskell no es C y es bastante legible, especialmente una vez que te acostumbras.También sigue el modelo de los algoritmos utilizados en muchos libros de texto, como Cormen.
Verá un montón de estilos de sangría diferentes para Haskell.La mayoría de ellos son muy difíciles de mantener sin un editor configurado para sangrar exactamente en cualquier estilo.
El estilo que muestra es mucho más simple y menos exigente para el editor, y creo que debería ceñirse a él.La única inconsistencia que puedo ver es que pones el primer do en su propia línea mientras pones los otros dos después de then/else.
Preste atención a los otros consejos sobre cómo pensar en el código en Haskell, pero cumpla con su estilo de sangría.