if / else控制块的Haskell编码风格好吗?
我正在学习Haskell,希望它可以帮助我更接近函数式编程。以前,我主要使用具有类似C语法的语言,例如C,Java和D。
我对Wikibooks教程使用的if / else控制块的编码风格有一个疑问。该代码如下所示:
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!"
这让我感到困惑,因为这种编码风格完全违反了类C语言中的推荐风格,在这种情况下,我们应该在同一列缩进" if"," else if"和" else"。
我知道这在Haskell中是行不通的,因为如果在与if相同的列中缩进else,那将是解析错误。
但是下面的样式呢?我认为这比上面的要清楚得多。但是,由于以上内容已被Wikibooks和Haskell官方网站上标记为"在线上最佳的在线指南"的" Haskell另一个教程"使用,所以我不确定这种编码风格是否是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!"
因此,我很好奇哪种编码风格使用得更频繁,或者这段代码是否还有另一种编码风格?
解决方案
我们还可以使用带有花括号的显式分组。请参阅http://www.haskell.org/tutorial/patterns.html的布局部分
我不建议这样做。除了少数特殊情况外,我从未见过有人使用显式分组。我通常会在Standard Prelude代码中查看样式示例。
我们可以使用" 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!"
对mattiast的case语句的一个小改进(我会编辑,但我缺乏因果报应)是使用compare函数,该函数返回LT,GT或者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!"
我真的很喜欢这些Haskell问题,并鼓励其他人发表更多。通常,我们觉得必须有一种更好的方式来表达想法,但是Haskell最初是如此陌生,以至于没有任何想法。
Haskell旅人的奖励问题:doGuessing是什么类型?
我使用的是我们Wikibooks中的示例一样的编码样式。当然,它没有遵循C准则,但是Haskell却没有C准则,而且可读性很强,尤其是一旦我们习惯了。它也是按照许多教科书(例如Cormen)中使用的算法样式进行仿造的。
请注意,我们必须在'do'块内缩进'then'和'else'的事实在很多人看来是一个错误。它可能会在Haskell规范的下一个版本Haskell'(Haskell素数)中修复。
Haskell样式是实用的,不是必须的!与其"先做然后做",不如考虑组合功能并描述程序将要做什么,而不是如何做。
在游戏中,程序要求用户进行猜测。正确的猜想是胜利者。否则,用户将再次尝试。游戏一直持续到用户猜对为止,所以我们这样写:
main = untilM (isCorrect 42) (read `liftM` getLine)
这使用了一个反复运行动作的组合器(在这种情况下,getLine提取一行输入,而read将该字符串转换为整数)并检查其结果:
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
谓词(部分应用在main
中)根据正确的值检查猜测并做出相应的响应:
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
直到玩家正确猜出为止要执行的动作是
read `liftM` getLine
为什么不保持简单,仅将两个功能组合起来呢?
*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
getLine的类型是IO String,但是read想要一个纯String。
来自Control.Monad的函数liftM
采用纯函数并将其提升为monad。表达式的类型告诉我们很多有关它的作用:
*Main> :type read `liftM` getLine read `liftM` getLine :: (Read a) => IO a
这是一个I / O操作,在运行时会给我们返回一个以read
转换的值,在本例中为Int
。回想一下," readLine"是一个产生" String"值的I / O操作,因此我们可以将" liftM"视为允许我们在" IO" monad中应用" read"。
示例游戏:
1 Too low! 100 Too high! 42 You Win!
Haskell在一个do块中解释if ... then ... else的方式与Haskell的整个语法非常一致。
但是许多人喜欢稍微不同的语法,允许" then"和" else"与相应的" if"以相同的缩进级别出现。因此,GHC附带了一种名为DoAndIfThenElse
的可选语言扩展,该扩展允许使用此语法。
在Haskell规范的最新版本Haskell 2010中,DoAndIfThenElse
扩展已成为核心语言的一部分。
我们将看到Haskell的一系列不同的缩进样式。如果没有将编辑器设置为完全以任何样式缩进的编辑器,它们中的大多数都很难维护。
我们显示的样式更加简单,对编辑器的要求也较低,我认为我们应该坚持使用它。我可以看到的唯一不一致之处是,我们将第一个do放在自己的行上,而另一个do则放在then / else之后。
请注意有关如何考虑Haskell中的代码的其他建议,但请遵循缩进样式。