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?

¿Fue útil?

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top