Haskell 中的缩进与 Python 中的缩进一样吗?

Is indentation in Haskell like in Python?

我刚开始学习 Haskell 并简要阅读了一些缩进规则,在我看来 Haskell 在缩进方面的表现与 Python 一样(我可能是错误的)。不管怎样,我试着写一个尾递归斐波那契函数,但我一直收到缩进错误,我不知道我的代码在哪里缩进了错误。

错误消息:

F1.hs:6:9: error:
    parse error (possibly incorrect indentation or mismatched brackets)
  |
6 |         |n<=1 = b   |         ^

代码:

fib :: Integer -> Integer
fib n = fib_help n 0 1
    where fib_help n a b
        |n<=1 = b
        |otherwise fib_help (n-1) b (a+b)

注意:我正在用 Notepad++ 编写代码,并且更改了设置,以便在我 TAB 时创建 4 个空格而不是制表符(我猜应该是这样)

两件事:

  1. 您需要排列管道以在辅助函数启动后发生。 Here is a good explanation on why this is.
  2. otherwise 后面还需要一个 = 符号 (otherwise is synonymous with True):
fib :: Integer -> Integer
fib n = fib_help n 0 1
    where fib_help n a b
           |n<=1 = b
           |otherwise = fib_help (n-1) b (a+b)

说 Haskell 的缩进类似于 Python 可能是一种过度概括,仅仅是因为语言结构截然不同。更准确的说法是,空格在 Haskell 和 Python.

中都很重要

你漏掉了一个“=”否则,你可以看到更多的例子here。正确的代码:

fib :: Integer -> Integer
fib n = fib_help n 0 1
    where fib_help n a b
            | n <=1 = b
            | otherwise = fib_help (n-1) b (a+b)

不,Haskell 缩进不像 Python。

Haskell 与缩进级别无关,而是让事物与其他事物对齐。

    where fib_help n a b

在这个例子中你有 where,下面的标记不是 {。这会激活布局模式(即空白敏感解析)。下一个标记 (fib_help) 设置以下块的起始列:

    where fib_help n a b
--        ^
--        | this is "column 0" for the current block

下一行是:

        |n<=1 = b

第一个标记 (|) 的缩进小于 "column 0",这隐含地关闭了块。

你的代码就像你写的一样被解析

fib n = fib_help n 0 1
    where { fib_help n a b }

        |n<=1 = b
        |otherwise fib_help (n-1) b (a+b)

这是几个语法错误:where 块缺少 =,并且您不能使用 |.

开始新的声明

解决方案:缩进应该属于 where 块的所有内容,多于 where 之后的第一个标记。例如:

fib n = fib_help n 0 1
    where fib_help n a b
            |n<=1 = b
            |otherwise = fib_help (n-1) b (a+b)

或:

fib n = fib_help n 0 1
    where
    fib_help n a b
        |n<=1 = b
        |otherwise = fib_help (n-1) b (a+b)

或:

fib n = fib_help n 0 1 where
    fib_help n a b
        |n<=1 = b
        |otherwise = fib_help (n-1) b (a+b)

您可以想象 Haskell 和 Python 的缩进类似,但有一些小差异。

然而,最大的区别可能是 Python 的缩进敏感语法总是在新行上开始对齐的块,而 Haskell 具有允许开始的对齐块的语法结构通过现有线路的一部分。这并不是布局规则的真正差异,但它会极大地影响您对它们的看法(Haskell 用户不倾向于在他们的头脑中将规则简化为 "indent levels") .

这是 Python 中一些(可怕的)布局敏感语法的示例:

if True:
    x = 1
    while (
not x
  ):
     y = 2

ifwhile 结构后跟一组对齐的语句。字符 next 语句的第一个非空白字符必须缩进到比外部块的对齐位置更远的某个位置,并为同一内部块中的所有后续语句设置对齐方式。每个语句的第一个字符必须与封闭块的某个位置对齐(这决定了它属于哪个块)。

如果我们在与位置 0 对齐的位置添加一个 z = 3,它将成为全局 "block" 的一部分。如果我们添加它对齐到位置 4,它将成为 if 块的一部分。如果我们将它添加到与位置 5 对齐的位置,它将成为 while 块的一部分。在任何其他位置开始语句将是语法错误。

另请注意,存在对齐完全不相关的多行结构。在上面,我使用括号在多行中写了 while 的条件,甚至将带有 not x 的行与位置 0 对齐。引入缩进块的冒号在 [= 上甚至无关紧要90=]行;缩进块的相关对齐是 while 语句的第一个非空白字符的位置(位置 4),以及下一个语句的第一个非空白字符的位置(位置 5)。

这里有一些(可怕的)布局敏感 Haskell:

x = let
   y = head . head $ do
              _ <- [1, 2, 3]
              pure [10]
   z = let a = 2
           b = 2
    in a * b
 in y + z

这里我们有 let(两次)和 do 引入对齐块。 x 的定义本身是构成模块的 "block" 定义的一部分,并且需要在位置 0.

let 块中第一个定义的第一个非空白字符设置块中所有其他定义的对齐方式。在外部 let 块中,位置 3 是 y。但是 let 的语法在开始缩进块之前不需要换行符(因为 Python 的缩进通过以冒号和新行结束 "header" 来构造所有的操作)。内部 let 块有 a = 2 紧跟在 let 之后,但 a 的位置仍然为块 (11) 中的其他定义设置所需的对齐方式。

同样,有些内容您可以拆分成多行,而这些行不需要对齐。在 Haskell 中,您几乎可以使用任何不是特定布局敏感结构的内容来执行此操作,而在 Python 中,您只能使用括号或以反斜杠结束一行。但是在 Haskell 中,构成结构一部分的所有行都必须比它们所属的块缩进得更远。例如,我能够将 z 定义的 in a * b 部分放在单独的一行中。 inlet 句法构造的一部分,但它是 不是 let 引入的对齐定义块的 部分,因此它没有特殊对齐要求。但是 z = ... 的整个定义是 outer let 定义块的一部分,所以我无法在位置 3 或 in a * b 行开始更早;它是 z 定义的 "continuation line",因此需要比该定义的开头进一步缩进。这与 Python 的续行不同,后者对缩进没有任何限制。

do 还引入了对齐块("statements" 而不是定义)。我本可以立即从 do 开始遵循第一条语句,但我选择另起一行。这里的块的行为很像 Python 风格的预期块;我必须在比外部块更缩进的某个位置开始它(外部 let 在位置 3 的定义),一旦我完成了 do 块中的所有语句必须对齐到相同的位置(此处为 14)。由于 pure [10] 之后的下一行是 z = ... 从位置 3 开始,它隐含地结束了 do 块,因为它与 let 块的定义对齐,而不是 do 块的语句。

在你的例子中:

fib :: Integer -> Integer
fib n = fib_help n 0 1
    where fib_help n a b
        |n<=1 = b
        |otherwise fib_help (n-1) b (a+b)

需要对齐的构造是 where,它引入了一个非常类似于 let 的定义块。使用 Python 样式的块,您总是在开始一个新块之前开始一个新行,您的示例将如下所示:

fib :: Integer -> Integer
fib n = fib_help n 0 1
    where
          fib_help n a b
        |n<=1 = b
        |otherwise fib_help (n-1) b (a+b)

这让错误更容易跳出来。您没有在 where 中的下一个 "indent level" 4 个空格处开始定义块,您是在位置 10 处开始的!然后回到第8位进行下一个"indent level".

如果你认为 Python 风格的 "indent levels" 比 Haskell 风格的对齐方式更舒服,只需格式化你的 Haskell 挡路 Python 要求您格式化其块;在 "header" 引入一个块之后,始终结束该行,然后在下一行的下一个 "tab stop".

处开始该块