30

我正在学习 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 中不起作用,因为如果我缩进elseif.

但是下面的风格呢?我认为它比上面的要清楚得多。但是由于上面的内容被 Wikibooks 和 Yet Another Haskell Tutorial 使用,在 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!"

所以,我很好奇哪种编码风格更常用——或者这段代码是否还有另一种编码风格?

4

8 回答 8

27

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> :类型读取。获取线

<交互式>:1:7:
    无法匹配预期的类型 `a -> String'
           针对推断类型“IO String”
    在 `(.)' 的第二个参数中,即 `getLine'
    在表达式中: read 。获取线

的类型getLineIO String,但read要纯的String

Control.Monad 中的函数liftM采用纯函数并将其“提升”为 monad。表达式的类型告诉我们很多关于它的作用:

*Main> :type read `liftM` getLine
读取 `liftM` getLine :: (Read a) => IO a

这是一个 I/O 操作,在运行时会返回一个用 转换的值readInt在我们的例子中是一个。回想一下,这readLine是一个产生String值的 I/O 操作,因此您可以将其liftM视为允许我们在read“内部”应用IOmonad。

示例游戏:

1
太低!
100
太高!
42
你赢了!
于 2010-01-19T18:52:21.287 回答
8

您可以使用“案例”结构:

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!"
于 2008-09-24T21:58:54.610 回答
8

mattiast 的 case 语句的一个小改进(我会编辑,但我缺少 karma)是使用 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 journyman 的额外问题:doGuessing 的类型是什么?

于 2008-09-29T01:46:11.493 回答
4

if ... then ... elseHaskell在块中解释的方式do非常符合 Haskell 的整个语法。

但是很多人更喜欢稍微不同的语法,允许thenelse出现在与相应的 . 相同的缩进级别if。因此,GHC 带有一个名为 的可选语言扩展DoAndIfThenElse,它允许这种语法。

在 Haskell 规范的最新版本Haskell 2010中,该DoAndIfThenElse扩展成为核心语言的一部分。

于 2010-01-19T22:36:59.570 回答
3

请注意,您必须在“do”块中缩进“then”和“else”这一事实被许多人认为是一个错误。它可能会在 Haskell 规范的下一个版本 Haskell'(Haskell prime)中得到修复。

于 2008-11-11T23:40:41.743 回答
1

您还可以使用带有花括号的显式分组。请参阅http://www.haskell.org/tutorial/patterns.html的布局部分

不过我不建议这样做。除了在一些特殊情况下,我从未见过有人使用显式分组。我通常查看标准前奏代码以获取样式示例。

于 2008-09-24T13:51:17.420 回答
0

我使用的编码风格就像您在 Wikibooks 中的示例一样。当然,它不遵循 C 指南,但 Haskell 不是 C,而且它的可读性很强,尤其是在你习惯了它之后。它也模仿了许多教科书(如 Cormen)中使用的算法风格。

于 2008-10-31T19:36:42.080 回答
0

你会看到 Haskell 有很多不同的缩进样式。如果没有设置为以任何样式精确缩进的编辑器,它们中的大多数都很难维护。

你展示的风格要简单得多,对编辑器的要求也不高,我认为你应该坚持下去。我能看到的唯一不一致之处是您将第一个 dos 放在自己的行上,而将其他 dos 放在 then/else 之后。

注意关于如何在 Haskell 中思考代码的其他建议,但要坚持你的缩进风格。

于 2010-08-29T10:21:01.223 回答