Bon style de codage Haskell du bloc de contrôle if / else?
-
02-07-2019 - |
Question
J'apprends Haskell dans l'espoir que cela m'aidera à me rapprocher de la programmation fonctionnelle. Auparavant, j'utilisais principalement des langages utilisant une syntaxe de type C, comme C, Java et D.
J'ai une petite question sur le style de codage d'un bloc de contrôle si
/ else
utilisé par le tutoriel sur Wikibooks . Le code ressemble à ceci:
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!"
Cela me laisse perplexe, car ce style de codage viole totalement le style recommandé dans les langages de type C, où nous devrions indenter si
, sinon si
et . sinon
dans la même colonne.
Je sais que cela ne fonctionne tout simplement pas dans Haskell, car ce serait une erreur d'analyse si j'indenterais else
dans la même colonne que si
.
Mais qu'en est-il du style suivant? Je pense que c'est beaucoup plus clair que celui ci-dessus. Mais puisque ce qui précède est utilisé par Wikibooks et Yet Another Haskell, le didacticiel qui porte la mention "meilleur didacticiel disponible en ligne". sur le site Web officiel de Haskell, je ne sais pas si ce style de codage est une convention dans les programmes 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!"
Alors, je suis curieux de savoir quel style de codage est utilisé plus souvent - ou existe-t-il un autre style de codage pour ce morceau de code?
La solution
Le style Haskell est fonctionnel, pas impératif! Plutôt que de "faire ceci, alors", Pensez à combiner des fonctions et à décrire ce que votre programme fera, pas comment.
Dans le jeu, votre programme demande à l'utilisateur de deviner. Une supposition correcte est un gagnant. Sinon, l'utilisateur essaie à nouveau. Le jeu continue jusqu'à ce que l'utilisateur devine correctement. Nous écrivons donc ceci:
main = untilM (isCorrect 42) (read `liftM` getLine)
Ceci utilise un combinateur qui exécute plusieurs fois une action ( getLine
extrait une ligne d'entrée et read
convertit cette chaîne en un entier dans ce cas) et vérifie son résultat:
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
Le prédicat (partiellement appliqué dans main
) compare la conjecture à la valeur correcte et répond en conséquence:
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
L'action à exécuter jusqu'à ce que le joueur devine correctement est
read `liftM` getLine
Pourquoi ne pas rester simple et composer les deux fonctions?
*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
Le type de getLine
est Chaîne d'E / S
, mais lu
veut une Chaîne
pure.
La fonction liftM
de Control.Monad prend une fonction pure et & # 8220; lève & # 8221; # 8221; dans une monade. Le type de l'expression nous en dit long sur ce qu'il fait:
*Main> :type read `liftM` getLine read `liftM` getLine :: (Read a) => IO a
Il s’agit d’une action d’entrée / sortie qui, une fois exécutée, nous renvoie une valeur convertie avec read
, un Int
dans notre cas. Rappelez-vous que readLine
est une action d'E / S qui génère des valeurs String
, de sorte que vous pouvez penser à liftM
comme nous permettant d'appliquer en lecture
& # 8220; à l'intérieur & # 8221; la monade IO
.
Exemple de jeu:
1 Too low! 100 Too high! 42 You Win!
Autres conseils
Vous pouvez utiliser le "cas" -construct:
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!"
Une amélioration mineure de l'instruction case de mattiast (je l'éditerais, mais le karma me manque) consiste à utiliser la fonction de comparaison, qui renvoie l'une des trois valeurs, LT, GT ou 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!"
J'aime beaucoup ces questions sur Haskell et j'encourage les autres à en publier davantage. Souvent, vous avez l’impression qu’il a obtenu d’être un meilleur moyen d’exprimer ce que vous pensez, mais Haskell est initialement si étranger que rien ne me vient à l’esprit.
Question de prime pour le journaliste de Haskell: quel est le type de travail à suivre?
La façon dont Haskell interprète si ... then ... else
dans un bloc do
correspond très bien à la syntaxe entière de Haskell.
Mais beaucoup de gens préfèrent une syntaxe légèrement différente, permettant à then
et à else
d'apparaître au même niveau d'indentation que le if correspondant
. Par conséquent, GHC est fourni avec une extension de langage opt-in appelée DoAndIfThenElse
, qui autorise cette syntaxe.
L'extension DoAndIfThenElse
est intégrée au langage principal dans la dernière révision de la spécification Haskell, Haskell 2010 .
Notez que le fait de mettre en retrait le "alors" et le "sinon" à l'intérieur d'un bloc "do" est considéré par beaucoup comme un bogue. Il sera probablement corrigé dans Haskell '(Haskell prime), la prochaine version de la spécification Haskell.
Vous pouvez également utiliser le regroupement explicite avec des accolades. Voir la section sur la disposition de http://www.haskell.org/tutorial/patterns.html
Je ne recommanderais pas cela cependant. Je n'ai jamais vu personne utiliser un groupement explicite à part quelques cas particuliers. Je regarde habituellement le code Prelude standard pour des exemples de style.
J'utilise un style de codage comme votre exemple de Wikibooks. Bien sûr, il ne suit pas les directives C, mais Haskell n’est pas C, et il est assez lisible, en particulier une fois que vous vous y êtes habitué. Il est également inspiré du style des algorithmes utilisés dans de nombreux manuels, comme Cormen.
Vous verrez un tas de styles d’indentation différents pour Haskell. La plupart d’entre eux sont très difficiles à maintenir sans un éditeur configuré pour indenter exactement quel que soit le style.
Le style que vous affichez est beaucoup plus simple et moins exigeant pour l'éditeur, et je pense que vous devriez vous en tenir à cela. La seule incohérence que je puisse constater est que vous mettez la première action sur sa propre ligne, tandis que vous placez les autres actions après l'option / else.
Suivez les autres conseils sur la manière de réfléchir au code dans Haskell, mais respectez votre style d'indentation.