关于Haskell代码的缩进,在 real world Haskell以及其他 Haskell的教程中只泛泛介绍了一些,还是有些让人迷惑。有人推荐了一篇网文,专门介绍Haskell的缩进,说的比较详细。原文在此: http://en.wikibooks.org/wiki/Haskell/Indentation 我试着翻译了一下,如下。
Haskell缩进
Haskell依靠缩进来简化冗长的代码,但它的缩进规则会让人有点儿迷惑,看起来很多而且很随意。其实混乱来自于规则在实际代码中的应用,而规则本身很简单,只有一两条。所以我们抛开实际中的缩进与排版的混乱表象,来看看规则的本质。
1 缩进的黄金规则
尽管本文要详细讲解Haskell的缩进体系,但是请记住一个最基本的规则:作为表达式的一部分代码,一定要比表达式的开头再缩进一些。
这是什么意思?看一个简单的let绑定表达式的例子。绑定变量的等式是let表达式的一部分,因此它要比作为表达式开头的let关键字更缩进一些。
.-----------------------------
| let
| x = a
| y = b
------------------------------
你当然可以只缩进一个空格,不过一般都会把第一个等式放在let的同一行,并把其余行缩进到与第一个等式对齐:
.-----------------------------
| let x = a
| y = b
------------------------------
还有更多的例子:
.-----------------------------
| do foo
| bar
| baz
|
| where x = a
| y = b
|
| case x of
| p -> foo
| p' -> baz
------------------------------
注意不像do和where表达式,在case表达式中,把第二行放到case的同一行没有意义。还有注意箭头(->)也被对齐了,这只是为了看上去漂亮一些,不是缩进排版所必须的。只有一行开头的空白才会改变排版,造成代码的不同含义。
如果一个表达式的开头并没有在一行的开始,缩进就稍微复杂一些。其实后面的代码只需比包含表达式开头的行更缩进一些就行。
.-----------------------------------------------------------------------------------------
| myFunction firstArgument secondArgument = do --'do'不在一行开始
| foo --这些行只需比包含'do'的行更缩进一些就行
| bar
| baz
------------------------------------------------------------------------------------------
上面的例子也可以这样写:
.-------------------------------------------------------
| myFunction firstArgument secondArgument =
| do foo
| bar
| baz
--------------------------------------------------------
或者:
.-------------------------------------------------------
| myFunction firstArgument secondArgument = do foo
| bar
| baz
-------------------------------------------------------
2 两种风格的转换
信不信缩进排版也不是必须的?是的,像C语言一样,Haskell也可以用分号来分割语句,用花括号来组织代码块。这种形式不仅常用,而且通过学习如何转换两种排版风格也有助于理解Haskell的缩进规则。首先要理解两个事情:何时需要分号和花括号,如何根据排版来转换。转换过程可以总结为3条规则(加上一条不太常用的第4条):
(1)遇到一个排版关键词(let,where,of,do),加一个左花括号。
(2)遇到具有相同缩进的句子,加一个分号。
(3)遇到一个退回缩进的句子,加一个右花括号。
(4)在list中遇到异常的句子,如where,不加分号,改加成右花括号。
练习1:用一个词回答:遇到一个更为缩进的句子时该加什么?
练习2:将下面的缩进排版翻译成分号花括号排版。注:有些语句可能不是Haskell的合法语句,本练习只是为了加强对翻译过程的理解。
of a
b
c
d
where
a
b
c
do
you
like
the
way
i let myself
abuse
these
layout rules
3 试试排版
.-------------------------
|错误的
|-------------------------
|if foo
| then do first thing
| second thing
| third thing
| else do something else
---------------------------
.-------------------------
|正确的
|-------------------------
|if foo
| then do first thing
| second thing
| third thing
| else do something else
---------------------------
if中的do
当if中出现do表达式时该怎么做?上面说过了,任何除过那4个排版关键字的符号,包括if then else,都不影响排版。所以缩进如下:
.-------------------------
|错误的
|-------------------------
|if foo
| then do first thing
| second thing
| third thing
| else do something else
---------------------------
.-------------------------
|正确的
|-------------------------
|if foo
| then do first thing
| second thing
| third thing
| else do something else
---------------------------
只要比第一行更缩进
记着,根据黄金规则,尽管do关键字告诉Haskell插入一个左花括号,花括号却是取决于do后面所跟的语句。下面这个看起来怪怪的代码块也是正确的:
do
first thing
second thing
third thing
也可以把if-do的联合写成这样:
.-------------------------
|错误的
|-------------------------
|if foo
| then do first thing
| second thing
| third thing
| else do something else
---------------------------
.-------------------------
|正确的
|-------------------------
|if foo
| then do
| first thing
| second thing
| third thing
| else do something else
---------------------------
do中的if
这段代码绊倒了许多Haskell程序员,看看为什么代码块是错的呢?
--为什么是错的?
do first thing
if condition
then foo
else bar
third thing
强调一下,这段代码中的if then else没有错,问题是当在do代码块中时,do注意到then语句部分和if语句部分有相同的缩进,导致它认为then语句是一个和if并列的新的语句。去掉语法糖,这就和其下边的写法一样:
.-------------------------
|一般排版
|-------------------------
|--错误版本
|do first thing
| if condition
| then foo
| else bar
| third thing
---------------------------
.-------------------------
|去掉语法糖后
|-------------------------
|--错误版本
|do { first thing
| ; if condition
| ; then foo
| ; else bar
| ; third thing }
---------------------------
这种写法使得编译器只看到了if condition;一样的语句,认为if表达式没有被完成,因此难怪会出错。改正方法就是把if块中的其他语句向后缩进一些,如下:
.-------------------------
|一般排版
|-------------------------
|--修正后的版本
|do first thing
| if condition
| then foo
| else bar
| third thing
---------------------------
.-------------------------
|去掉语法糖后
|-------------------------
|--修正后的版本
|do { first thing
| ; if condition
| then foo
| else bar
| ; third thing }
---------------------------
if后的缩进防止了do代码块错误的将then部分解释为一个新的语句。你也可以对每一个then else进行这样的缩进,虽然有时候这并不是必须的,但会避免很多歧义,使程序更清晰。
练习:do中if的缩进问题困扰了很多Haskell编程者,有人向Haskell的主要设计人员提议在if then else中也增加分号,你觉得这会有什么样的作用?