Search This Blog

Labels

Tuesday, December 21, 2010

Haskell 代码阅读技巧

1 Introduction

本手册是为那些想理解但并不关心怎么写 Haskell 代码的非 Haskell 程序员而写的。这里,我们分门别类的介绍一些技巧、窍门。尽管不建议跳跃式阅读,但本文无须顺序阅读。如果你认为本手册有哪些不妥,请告诉我们以改进这个手册。

注意:建议看一下 Haskell for C Programmers 来感受这两种语言的异同。

2 General advice

2.1 Tip: it's just very very concise

Haskell代码难以阅读其中的一个原因是它的代码太简洁了。Haskell的一小段代码信息量很大,以至于当我们碰到一个不懂的地方,就要反复琢磨一个语句的具体意思,但这对于熟悉 Haskell的程序员来说反而是一个优势。代码简洁就意味着Haskell函数较短,这样当看到一段非常复杂的代码时,就不会跨越很大的区域。就像一个硬币有正反两面一样:

反面:高密度  ==  每行代码花费时间较多

正面:简洁明了 == 较少的代码花费时间较少

在一定时间内理解小代码段是不容易的,但又是值得去做的。较小的代码段难以阅读意味着比较抽象,而抽象的东西就意味着可以适用很多地方。因此,虽然开始花费较长的时间去理解代码,但以后得到的回报会更多。

2.2 Trick: use the haddock

当阅读一段较长的、包含许多模块的 Haskell 代码时,最好使用一个能在一旁能自动生成 API 文档的代码浏览器。

3 What does this function do?

3.1 Trick: use type signatures

当看到如下的一段代码时:

map :: (a -> b) -> [a] -> [ b]

不要忽略它,因为这是类型签名,是得到函数功能的一个非常有用的途径。

从上面的例子,我们可以得到以下信息:map 这个函数以任意一个为(a -> b)类型的函数为参数,返回一个函数,这个返回的函数以一个元素为 a 类型的列表为参数,并且返回一个元素为b 类型的列表。因此,如果sqrt以一个数字为参数而返回这个数字的平方根,map sqrt 就是以若干数的列表为参数,返回其对应数字的平方根的列表。

另一个例子:

swap :: (a,b) -> (b,a)

swap 这个函数以一个类型为 (a, b) 的元组为参数,返回一个类型为 (b, a) 的元组

下面是 Haskell 中常见的类型签名:

fn :: (b -> c) -> Foo  -- fn 是一个高阶函数,把类型为 (b -> c) 的函数作为参数

fn :: x -> IO Int     -- fn 是一个返回一个 Int 的输入输出 action

fn :: x -> [y]                  -- fn 返回一个元素为 y 类型的列表

fn :: x -> (y,z)               -- fn 返回一个类型为 (y,z) 的元组

fn :: x                           -- fn 是一个值

3.2 Tip: Haskellers love pattern matching

head [x] = x

上面的意思是:如果 'head' 后跟一个只有一个元素的列表,就把这个元素绑定到 x,并且返回 x

另一个例子:

fst (x,y) = x

snd (x,y) = y

上面的函数是用来获取一个二元组的第一个和第二个元素的。很容易就可以看出它们是怎么工作的。

3.3 Tip: a function may be defined in more than one piece

还记得数学课上一个被定义为“如果 x >= 0,abs(x) = x,否则 abs(x) = -x”( abs(x) = x if x>= 0 or -x otherwise)的函数吗?其实Haskell 中也有类似的定义。有时,Haskell 程序员不写 if-then-else,为了方便,分两种情况来定义,比如:

abs x | x >= 0 = x

abs x = -x

但令人混淆的是下面的定义:

foo x | blah = some enormous long thing(一些冗长的东西)

foo x = some other enormously long thing(另一些冗长的东西)

特别是当看到下面的一个,很难记住旁边还有另一个 foo 定义。庆幸的是,你不需要看太远,(这个定义)要么在另一个定义的上面紧挨着,要么在下边。

(注意:一些程序员可能这样写代码:foo x | otherwise = ... 其中,otherwise 相当于True,在这里非常有用,因为这提示“foo 函数没有完整被定义”。)

3.4 Tip: pattern matching and guards can be mixed and matched

elaborate

combine ((f,a,b,r) : (f',a',b',r') : ss) | f == f' = combine ((f,a.+a',b.+b',r+r'):ss)

combine ((f,a,b,r) : ss) = (f,a,b,r) : combine ss

combine [] = []

4 What the heck is xyz?

当阅读 Haskell 代码时,你将会遇到有特别含义的 xyz 标识符。

4.1 Tip: the smaller the name, the smaller the scope

你讨厌在代码中使用像 Haskell 中x、xs 这样短小又无意义的标识符的编程方式吗?但是,Haskell 程序员使用这些标识符是理应如此的。

首先,这些短小又无意义的标识符是被使用在短小代码中的。我们可以看一个典型的(并不高效)素数产生器的实现:

primes :: [Integer]

primes = sieve [2..]
                where   sieve (p : xs) = p : sieve [x|x <- xs, x `mod` p > 0]

where 块中有一个使用 x、xs、p 标识符的函数。很明显,在一个更冗长的语言(verbose language)中阅读这样的代码很困难,因为在一个很长的代码块中查找这样的标识符是很困难的,比如,在 C 语言中,通常可能有许多(甚至上百个)变量定义在函数的顶部,这样,你可能用 known_prime 来代替 p,用candidate_primes 来代替 xs。

但在 Haskell 中并不需要这样做,x、xs、p 定义在和它们被使用的同一行。还有,这三个变量命名都遵循一个惯例:一个列表的标识符以 s结尾,一个单值(singular value)用一个字母命名。在上面,一个不常见的部分就是使用了 (p : xs) 这种形式,而没有使用更为常见的(x : xs),在此程序员向我们暗示:这个列表的第一个元素不同于常见的列表,一看到 p 就让我们联系到素数(prime number)。

Haskell 之所以可以这样表达并且很容易被理解,是因为其代码极其简洁。这种用法在更冗长的语言(verbose language)中一点也不适用,但是当你在 Haskell 中恰当的使用这种方式是一种享受。

以上并不是使用这些标识符的主要原因,另有更深层次的原因。Haskell 语言实现了函数组合和高阶函数(allows for unparallelled levels of abstraction through functional composition and higher-order functions)。在大多数语言中,函数是一个有特定具体功能的低层次实体,Haskell中的函数更抽象,看一下Prulude 模块中的 foldl 中的典型实现:

foldl   :: (a -> b -> a) -> a -> [ b] –> a

foldl f z []    = z

foldl f z (x : xs) = foldl f (f z x) xs

     

这个函数高度抽象,它实际上是迭代一个列表和计算并且汇总结果的一个抽象。其中列表可以为任何类型的列表,计算可以为任何一个你想用的计算,结果可以为任何初值类型的结果。如果使用一个有含义的标识符,将会使代码变得非常不容易阅读,例如下面的一段代码:

foldl binary_operation priming_value (list_head:list_body) =

      foldl

          binary_operation

          (binary_operation priming_value list_head)

          list_body

很明显,foldl 使用 Haskell 简单命名惯例的第一个实现比后者更具可读性。

4.2 Tip: types, functions and values

Haskell 中的类型变量通常被命名为 a、b 等等,但也有时(并不常见)尾追一些数字,比如 a1 或 b3。

作为高阶函数参数的函数通常命名冠以f、g 等,但有时也像类型变量一样尾追一些数字或者单引号 ' ,例如像 g',在以后的例子中,你可以把它读作 "Jee-prime",并且它被认为是一个与 g 函数有特定关系(a helper or the like)的函数。偶尔,函数可以被命名为 names that are not on this continuum as an aidememoir,例如,函数里作为谓词(predicate)的参数可以被命名为p。

函数的参数和小函数内部专用变量常常被命名为 x、y 等等,偶而尾追一些数字。当然,为了助记,我们可以使用其他字母来命名变量,例如,我们可以把一个值为素数(prime)的变量命名为p。

注意:这些只是一些指导性的建议,并不是规定。在写 Haskell 代码时,你可以不遵守、改变或滥用这些建议。(大致看一下Haskell 98 Report 中提供的标准Prelude 模块足够让你信服)

4.3 Tip: the -s and m- habits

短小的变量名有时遵循一个习惯。如果你把一个值命名为 x,通常这种类型的值的列表被命名为 xs(x的复数)。因此,如果看到一个命名为 as、bs或者 foos,这些通常被读作为 'aeyes'(a的复数)、bees'(b的复数)、'foohs'(foo的复数)。很明显,当我们看到 as 时,我们很容易知道它是 a 的一个列表。

相似的,你可能会碰到另一个命名习惯:变量名通常冠以 m-。很少有这种情况,但是如果碰到一段代码中有许多 m- 时,有可能这些值是 Maybe 类型。有时,一个类型为 Whatever 的值 foo,那么,mfoo 的类型就有可能是 MaybeWhatever。放心,这些不是匈牙利标记法。这些不是系统默认或硬性规定。

当同时有这两种变体时,这两种约定都非常有用。例如,当我们同时使用 Whatever 和 [Whatever](Whatever 的List),x,xs 命名是一个指出他们具有一些相同属性(只是一个为列表)的很好的方法,同样,当我们在一个函数中同时使用 Whatever 和 Maybe Whatever, x、mx 同样很好。

最后,库函数有时会以 'l'、'r'、'_'、'M'、'''(单引号)结尾,这些代表什么意思呢?

mapM   --作用于 monad 的 map 函数, 'M' 后缀暗示这个函数等同于相应的 pure 函数,但作用于 monad 的版本

mapM_  -- '_' 后缀暗示计算的结果被忽略,并且返回 ()。

foldl  -- 从左至右贯穿一个结构的 fold

foldr  -- 从右至左贯穿一个结构的 fold

foldl' -- 一个累加器为 strict 的 fold。 '  用来指定此为一个 strict 函数

4.4 Tip: order mostly doesn't matter

函数定义的顺序没有严格的要求,下面代码

foo x y z = ...

bar a b = ... foo b ...

写可以这样写:

bar a b = ... foo b ...

foo x y z = ...

前面定义的函数可以调用其后面的函数,反之亦然。不同函数可以被任意的顺序定义。

4.5 Tip: order does matter for pieces of functions

要提醒的是:函数间的定义顺序可以为任意顺序,但函数体内的定义顺序将影响函数的功能。

例如,下面的两个 abs 并不相同!

-- 正确的顺序

abs x | x >= 0 = x

abs x = -x

-- 错误的顺序

abs2 x = -x

abs2 x | x >= 0 = x

4.6 Trick: use grep

(这个方法看起来特别有用,但这个功能往往被忽略。)

除了使用文本编辑器的查询功能外,grep 查询也是比较快的。例如,用 vi 编辑器,可以用 /= *xyz 来查询一个等号 ,后跟任意个的空格,然后是 xyz 的一段字符串。

除此之外, xyz 可能被定义在其他模块内,你可能会看到以下语句:

import Manamana (xyz)

但注意:有的程序员为了偷懒,可能不明显的指定导入 xyz ,他们只是这样写:

import Manamana

因此为了解决这个问题,我们可以使用 grep xyz *.lhs *.hs 命令(注意文书式的程序有时使用非文书式的代码,所以要同时搜索 lhs 文件与 hs 文件)

另外一个建议是,如果你没有查询到任何东西,可以用 Hoogle.

最后一个建议是:

Hugs/WinHugs 使用者可以使用 :find 命令,":find xyz" 将会用你的文本编辑器打开那个模块,并且将光标跳到其定义的那一行。GHCi 使用者可以使用 ":i xyz" 来获取 "xyz" 的定义处(但不会打开文本编辑器)。

No comments:

Post a Comment