免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 6972 | 回复: 3
打印 上一主题 下一主题

Haskell 代码阅读技巧(译) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2009-04-26 16:51 |只看该作者 |倒序浏览
来个姊妹篇

本文可以被任意转载、优化修改,无须注明

译文中如有不妥之处,请参照原文
原文为:How to read Haskell
链接:http://www.haskell.org/haskellwiki/How_to_read_Haskell

译者翻译不精,译文中肯定有许多 bugs ,还望广大 CUer 指正

Contents
1 Introduction
2 General advice
    2.1 Tip: it's just very very concise
    2.2 Trick: use the haddock
3 What does this function do?
    3.1 Trick: use type signatures
    3.2 Tip: Haskellers love pattern matching
    3.3 Tip: a function may be defined in more than one piece
    3.4 Tip: pattern matching and guards can be mixed and matched
4 What the heck is xyz?
    4.1 Tip: the smaller the name, the smaller the scope
    4.2 Tip: types, functions and values
    4.3 Tip: the -s and m- habits
    4.4 Tip: order mostly doesn't matter
    4.5 Tip: order does matter for pieces of functions
    4.6 Trick: use grep

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" 的定义处(但不会打开文本编辑器)。

[ 本帖最后由 izhier 于 2009-4-29 18:49 编辑 ]

论坛徽章:
0
2 [报告]
发表于 2009-04-27 17:47 |只看该作者
不行,版主!这个要加精!

翻译这篇文章,我用了一个多周的零碎时间捏

论坛徽章:
0
3 [报告]
发表于 2009-04-29 17:25 |只看该作者
不错的文章。
小添个花。

Luckily, you never have to look veryfar, either immediately above or immediately below the otherdefinition.
庆幸的是,你不需要看太远,(这个定义)要么在另一个定义的上面紧挨着,要么在下边。

(Note that literate programssometimes use non-literate code, so search in both lhs AND hs)
(注意文书式的程序有时使用非文书式的代码,所以要同时搜索 lhs 文件与 hs 文件)

论坛徽章:
0
4 [报告]
发表于 2009-04-29 17:51 |只看该作者
谢谢楼上的翻译,我这就添加上去
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP