if / else控制块的Haskell编码风格好吗?

时间:2020-03-06 14:39:44  来源:igfitidea点击:

我正在学习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中的代码的其他建议,但请遵循缩进样式。