Haskell入门 - Delphi,Linux,Python - delphi入门
这篇文档是我前段时间在瞎搞搞Haskell的时候,从这里找到,为了方便自己看,我把它整理成了一篇文档,现在贴上来,方便刚学习Haskell的初学者。因为,原文看起来实在是麻烦。
——————————————————————————————
作者:David Mertz 是一名作家、程序员和教师
作者联系方式:mertz@gnosis.cx
正文开始:
我适合此教程吗?
本教程的对象是那些要了解用 Haskell 语言进行函数型编程的命令式语言程序员。如果您曾用 C、Pascal、Fortran、C++、Java、Cobol、Ada、Perl、TCL、REXX、JavaScript、Visual Basic 或其它许多语言编过程序,那么,您就一直在使用命令式范例。本教程用平和的语言介绍了函数型编程的范例,并用 Haskell 98 语言进行特定说明。
具有函数型编程背景的程序员可能会觉得本教程的进度有些缓慢;但是,特别对于那些从未使用过 Haskell 98 的程序员来说,仍能通过浏览本教程对这一语言有个大致了解。
未涉及哪些内容?
在介绍性教程中,无法涉及 Haskell 的许多强大和复杂的特征。尤其是,类型类和代数类型(包括抽象数据类型)的整个范围对于初次介绍有点过多。对于那些兴趣没有得到满足的读者,我会指出 Haskell 允许您创建自己的数据类型,并继承类型实例中那些数据类型的特性。Haskell 类型系统包含了面向对象编程的基本特性(继承、多态性、封装);但是它所用的方法在 C++/Java/Smalltalk/Eiffel 编程思想方法中几乎是不可领会的。
本教程中省略的另一个重要元素是对单目的讨论,因此也不讨论 I/O。编写的教程甚至不以“Hello World!”程序开始好象挺奇怪,但以函数型编程思想方法考虑需要一些变化。虽然“Hello World!”相当简单,但它也牵涉到单目的“伪命令”世界。初学者容易被伪命令样式的 I/O 所哄骗,并忘掉真正要做什么。学习游泳的最好方法就是跳进水中。
关于 Haskell
Haskell 只是众多函数型编程语言中的一个。其它包括 Lisp、Scheme、Erlang、Clean、Mercury、ML、OCaml 等等。常用的附属语言 SQL 和 XSL 也是函数型的。与函数型语言相同,诸如 Prolog 语言等逻辑型或基于约束的语言都是声明型的。相反,过程和面向对象的语言(广义上)都是命令式的。有些语言(如 Python、Scheme、Perl 和 Ruby)跨越这些范例边界;但是在很大程度上,编程语言都有一个特定的主要关注焦点。
在函数型语言中,Haskell 在许多方面都是最理想化的语言。Haskell 是一个纯函数型语言,这意味着它避开了所有的副作用(后面会详细说明)。Haskell 有一个非严格或延迟求值模型,而且它有严格的类型(但是有一些允许特定的多态性)。其它函数型语言在这些特性中有所不同 — 因为这对它们的设计理念很重要 — 但是这种特性集合会使人们(有待论证)最大程度地以函数型方式考虑程序。
有一点要略加注意,Haskell 在语法上比列表派生的语言(特别是对于那些轻松地使用诸如 Python、TCL 和 REXX 等标点分隔的语言的程序员)更容易理解。大多数运算符都是插入词,而不是前缀。缩排和模块组织看上去相当熟悉。最引人注目的可能是,避免了嵌套括号过分地深(如我们在 Lisp 中所见到那样)。
获得 Haskell
Haskell 有几个用于多平台的实现。它们包括一个名为 Hugs 的解释型版本和几个 Haskell 编译器。所有这些的最佳起点是 Haskell.org。链接会引导您通向各种 Haskell 实现。根据您的操作系统以及它的打包系统,可能已安装了 Haskell,或者可能有一个标准方法来安装运行就绪的版本。建议那些学习本教程的人员如果要开始实践并与本教程一起执行的话,就获取 Hugs,如果您愿意的话。
作出放弃
用 Haskell 开始编程的最困难部分就是放弃在命令式编程思想中的许多非常熟悉的技术和方法。第一印象常常是:如果您不能处理 X、Y 或 Z,特别是在 X、Y 和 Z 是“常规”命令式编程中最常用的模式时,那么编写一个计算机程序一定是完全不可能的。在这一节中,让我们看一看 Haskell(和一般的函数型编程) 最“令人震惊”的一些特性。
没有可变变量
在命令式编程中最常用的编程习惯之一是:将一个值赋给一个变量,然后将另一个值赋给该变量;可能在这个方法中,而这可能就发生在我们测试该变量是否获得了某些键值的过程中。象下面的 C 示例那样的结构是随处可见的(其它命令式语言都是类似的):
if (myVar==37) {…}
myVar += 2
for (myVar=0; myVar<37; myVar++) {...}
相反,在 Haskell 中,这类变量根本不存在。可以将一个名称与一个值绑定,但一旦指定,这个名称在整个程序中只表示这个值。不允许任何更改。
在 Haskell 中,“变量”与数学等式中的变量非常相似。它们需要满足特定规则,但是它们不是命令式编程样式中的“计数器”或“容器”。为了先大致了解什么是正确思维,请考虑下面的线性方程以获取灵感:
10x + 5y - 7z + 1 = 0
17x + 5y - 10z + 3 = 0
5x - 4y + 3z - 6 = 0
在这类描述中,我们有“未知数”,但这些未知数在我们计算它们时并不更改它们的值。
隔离副作用
在 Haskell 中,函数计算在程序中不能有副作用。命令式程序中的多数副作用大概都是上一屏中提及的变量重新赋值那一类(不管是全局变量、局部变量还是字典、列表或其它存储结构),但是每个 I/O 事件也是一种副作用。I/O 改变了世界而不是计算本身的一部分。自然地,许多时候您希望以某种方法改变世界(如果不这样,甚至无法知道程序已在运行)。Haskell 将所有这样的副作用限制在一个称为单目 IO 的狭小“框”中。单目中的任何事物都无法出来,而单目外的任何事物也无法进入。
通常,结构化的命令式编程接近了函数型编程限制 I/O 的目标。好的设计可能要求输入和输出只在一组有限的适当命名的函数中发生。很少有结构化编程会到处和以难以预知的方式对 STDIO、文件、图形设备等进行读写。函数型编程将这个界限提到一个高得多的级别。
没有循环
Haskell 另一个有趣的特性是它没有任何循环结构。没有 for 和 while。没有 GOTO 或 branch 或 jmp 或 break。人们可能认为在没有这种基本(命令式)结构的情况下,不可能控制程序去执行什么;但是摆脱这些结构实际上是彻底的解放。
没有循环实际上等同于没有副作用问题。因为一次循环中的变量值不能与另一次循环中的变量值不同,所以无法区分它们;而且通常是为了执行另一个程序活动时才需要分支。因为函数型编程没有活动,只有定义,那么还有分支干扰吗?
但是,我应该尝试站在公正的立场。实际证明可以模拟几乎所有的常见循环结构,常常使用与其它语言相同的关键字,并且其样式与命令式结构惊人地相似。Simon Thompson 在他的书籍中提供了许多这样的示例(请参阅本教程末尾处的“参考资料”)。
没有程序次序
Haskell 没有的(或不需要的)另一样事物是程序次序。组成程序的一组定义可以任何次序发生。当定义看上去非常象命令式语言中的赋值时,这可能有点奇怪。例如,这样看上去好象有问题:
– Program excerpt
j = 1+i
i = 5
– Hugs session after loading above program
– Main> i
– 5 :: Integer
– Main> j
– 6 :: Integer
要理解与上面程序相似的程序,需要知道 i 和 j 并不是赋的值,而是以给定方式定义的。事实上,在上例中,i 和 j 都是函数,上面的示例都是函数定义。在许多命令式编程语言中,也不允许多次定义函数(至少在同一作用域中)。
Haskell 程序中有什么?
一个 Haskell 程序基本上由一组函数定义组成。函数与名称的绑定方法非常类似于其它语言中的变量赋值。但是,这实际并不相同;一个 Haskell 绑定名称更类似于数学证明中的绑定。在数学证明中,我们可以说“让 tau 表示一个等式 …”名称绑定只提供一个简写供等式稍后使用,但是名称在一个程序中只能绑定一次 — 尝试更改它时会发生程序错误。
myNum :: Int — int myNum() {
myNum = 12+13 — return 12+13; }
square :: Int -> Int — int square(int n) {
square n = n*n — return = n*n; }
double :: Int -> Int — int double(int n) {
double n = 2*n — return 2*n; }
dubSqr :: Int -> Int — int dubSqr(int n) {
dubSqr n = square (double n) — return square(double(n)); }
定义函数
一个函数定义有两个可选部分。第一个部分(概念上,在清单中不是必需的)是函数的类型说明。在函数中,类型说明定义输入的所有类型和输出的类型。在示例中的行尾注释中提供了一些类似的 C 定义。
函数定义的第二部分是函数的实际计算。在这个部分中,通常(但非总是)将一些特别变量提供给等号左侧,这些变量参与对右侧的计算。但是,与 C 中的变量不同,Haskell 变量更象是数学中的变量,它指等号两边完全相同的“未知量”(而不引用保存值的“容器”)。
在函数定义中,完全绕过变量的明确命名常常也是可能的。在 dubSqr2 中,完全可以表示不管对什么事物进行 double 操作,我们都应该进行 square 运算。对此,不需要提及变量名,因为经过 dubSqr2 的事物只是那些在以后的表达式中跟在绑定名称的表达式。当然,double 本身必须采用 dubSqr2 期望的同一输入类型,然后输出 square 需要作为输入的输出类型。
example :: Int
example = double (myNum - square (2+2))
dubSqr2 :: Int -> Int
dubSqr2 = square . double — Function composition
更简单的函数定义
象 C 一样,Haskell 有严格的类型。averageThree 是一个很好的函数示例,为了返回正确的值类型,它需要对类型进行强制转换。但是,difSquare 函数显示了与 Haskell 的一些差异。difSquare 没有类型说明,所以 Haskell 将根据函数定义中涉及的操作来推断适当的类型说明。首先从外表看,它好象与那些动态类型或类型宽松的语言执行的相同;但是 Haskell 所执行的完全不同。difSquare 在编译时(对此没有运行时动态一说)是有严格类型的,但是 difSquare 的类型有一个类型类,它包括了整数和浮点数(还有有理数和复数等)。我们可以在 Hugs 中找到推理类型:
Main> :type difSquare
difSquare :: Num a => a -> a -> a
也就是,将两个输入自变量和输出都推断为具有“类型类”Num。如果我们显式地声明一个诸如 Int 的类型,则函数将对更小范围的值进行操作(根据我们的需要,这样做有利有弊)。
– Average of three Integers as floating point
averageThree :: Int -> Int -> Int -> Float
averageThree l m n = fromInt(l+m+n) / 3
— float averageThree(int l, int m, int n) {
— return ((float)(l+m+n))/3; }
difSquare x y = (x-y)^2 — C lacks polymorphic type inference
递归
没有循环结构,Haskell 程序中的流程通常表示为递归。依据递归考虑所有流程要花一些工作,但是它证实是与其它语言中的 while 和 for 结构具有恰恰相同的表达和强大功能。
递归的诀窍是我们希望它最终终止(至少通常是这样的)。保证递归终止的一种方法是使用原语递归。它实际使一个“事物”递归,并确保下一个调用比当前的调用更接近终止条件。实际上,要确保这样,我们可以在每次调用时递减一个整数(并且在零或其它目标时终止),或对每个连续调用只取列表的末尾(并在列表为空时终止)。示例中列出的两个版本的阶乘都假设它们将传递一个大于零的整数(否则,会失败;练习:如何进行?)。
也存在非原语递归,但是很难确定知道递归将终止。还允许函数之间的相互递归(常会遇到),但是原语递归仍是最安全最常用的形式。
– Factorial by primitive recursion on decreasing num
fac1 :: Int -> Int
fac1 n = if n==1 then 1 else (n * fac1 (n-1))
– Factorial by primitive recursion on list tail
fac2 :: Int -> Int
fac2 n = prodList [1 .. n]
prodList lst =
if (length lst)==1 then head lst
else head lst*(prodList (tail lst))
模式匹配
在函数型编程中,我们“更多地会关心如何定义而不是如何计算的细节”(但是要有保留地记住这句座右铭,效率在某些情况下仍很要紧)。思路是:得出如何获得解决方案是编译器或解释器的工作,而不是让程序员去做。
指定如何定义一个函数的有用方法是描述在给定各种输入类型的情况下,它将返回什么样的结果。在 Haskell 中描述“输入的各种类型”的功能强大的方法是使用模式匹配。我们可以为一个函数提供多个定义,每个定义都有一个对输入自变量的特定模式。成功与给定的函数调用匹配的第一个列出的定义就是用于该调用的。用这种方法,可以抽出列表的头尾,匹配特定的输入值,将空的列表标识为自变量(通常用于递归)并分析其它模式。但是,不能用模式匹配执行值比较(例如,必须以不同方法检测“n <= 3”)。下划线应该在与某内容匹配的位置处使用,而该位置的匹配值不在定义中使用。
prodLst2 [] = 0 — Return 0 as product of empty list
prodLst2 [x] = x — Return elem as prod of one-elem list
prodLst2 (x:xs) = x * prodLst2 xs
third (a,b,c,d) = c — The third item of a four item tuple
three = third (1,2,3,4) — ‘three’ is 3
– Is a sequence a sub-sequence of another sequence?
isSubseq [] _ = True
isSubseq _ [] = False
isSubseq lst (x:xs) = (lst==start) || isSubseq lst xs
where start = take (length lst) (x:xs)
保护信息
有些类似于模式匹配,并且与 if .. then .. else 也相似,这些结构(在前期示例中看到的)在函数定义中是保护信息。保护信息只是可能获取的条件和属于该情况的函数的定义。任何可以用模式匹配声明的事物也可以改述为保护信息,但是保护信息还允许使用另外的测试。只要是第一个匹配的保护信息(按列出的次序),就成为特定应用程序的函数定义(其它保护信息也可能匹配,但如果它们列在后面,则它们就不用于调用)。
在效率方面,如果可能,模式匹配通常是最佳的。经常可能将保护信息与模式匹配相结合,如 isSublist 示例中。
prodLst3 lst — Guard version of list product
| length lst==0 = 0
| length lst==1 = head lst
| otherwise = head lst * prodLst3 (tail lst)
– A sublist is a string that occurs in order, but not
– necessarily contiguously in another list
isSublist [] _ = True
isSublist _ [] = False
isSublist (e:es) (x:xs)
| e==x && isSublist es xs = True
| otherwise = sublist (e:es) xs
列表包含
Haskell 中功能最强的结构之一是列表包含(从数学家的角度看:本术语源自 Zermelo-Frankel 集合论的“包含公理”)。与其它函数型语言相似,Haskell 除了操作列表外,还构建了许多功能。但是,在 Haskell 中,可能会生成一份压缩格式的列表,简单声明列表元素的来源以及元素满足哪些标准。用列表包含描述的列表必须从其它原始列表中生成;但是幸运地是,Haskell 还提供了一个快速“枚举”语法来指定原始列表。
– Odd little list of even i’s, multiple-of-three j’s,
– and their product; but limited to i,j elements
– whose sum is divisible by seven.
myLst :: [(Int,Int,Int)]
myLst = [(i,j,i*j) | i <- [2,4..100],
j <- [3,6..100],
0==((i+j) `rem` 7)]
– Quick sort algorithm with list comp and pattern matching
– ‘++’ is the list concatenation operator; we recurse on both
– the list of “small” elements and the list of “big” elements
qsort [] = []
qsort (x:xs) = qsort [y | y<-xs, y<=x] ++ [x] ++ qsort [y | y<-xs, y>x]
延迟求值 I
在命令式语言中(及在某些函数型语言中),表达式求值是精确且直接的。例如,如果用 C 语言编写 x = y+z;,则您告诉计算机马上进行计算,并将一个值放入名为“x”的内存中!(无论何时遇到这一代码)。相反,在 Haskell 中,求值是延迟的 — 仅在差不多需要表达式求值时,才对表达式求值(很明显,C 包含了布尔表达式的快捷方法,它是一种很小的延迟)。示例中的 f 和 g 的定义显示了这种差异的简单形式。
象 g 那样的函数有点愚蠢,因为恰好不使用 y,具有模式匹配或保护信息的函数经常仅在某些情况下才会使用特定自变量。如果一些自变量有一定的特性,则对于给定计算,它们或其它自变量可能不是必需的。在这种情况下,不执行不必要的计算。而且,当以计算方法(列表包含和枚举省略号形式)表示列表时,实际只计算那些真正使用的列表元素。
f x y = x+y — Non-lazy function definition
comp1 = f (4*5) (17-12) — Must compute arg vals in full
g x y = x+37 — Lazy function definition
comp2 = g (4*5) (17-12) — ‘17-12′ is never computed
– Lazy guards and patterns
– Find the product of head of three lists
prodHeads :: [Int] -> [Int] -> [Int] -> Int
prodHeads [] _ _ = 0 — empty list give zero product
prodHeads _ [] _ = 0
prodHeads _ _ [] = 0
prodHeads (x:xs) (y:ys) (z:zs) = x*y*z
– Nothing computed because empty list matched
comp3 = prodHeads [1..100] [] [n | n <- [1..1000], (n `rem` 37)==0]
– Only first elem of first, third list computed by lazy evaluation
comp4 = prodHeads [1..100] [55] [n | n <- [1..1000], (n `rem` 37)==0]
延迟求值 II
关于 Haskell(和延迟求值)有一点真的值得注意,那就是可能使用无限列表。不仅仅是大型列表,而是真的无限!当然,这个诀窍就是不明确计算列表中特定计算不需要的部分(只是由运行时环境保持它们扩展的规则)。
找到素数的著名且古老的算法是“爱拉托逊斯筛法”。思路是保持整数列表的初始元素,但根据可能的素数去掉它的倍数。示例就是这样做的,但是它只是按特定计算需要执行。但是,列表 prime 实际就是全体素数的列表。
– Define a list of ALL the prime numbers
primes :: [Int]
primes = sieve [2 .. ] — Sieve of Eratosthenes
sieve (x:xs) = x : sieve [y | y <- xs, (y `rem` x)/=0]
memberOrd :: Ord a => [a] -> a -> Bool
memberOrd (x:xs) n
| x
| x==n = True
| otherwise = False
isPrime n = memberOrd primes n
– isPrime 37 is True
– isPrime 427 is False
第一类函数(传递函数)
Haskell(及所有函数型编程)的一个强大特性就是函数都是第一类的。函数的第一类状态表示函数本身只是值。就象您将一个整数作为自变量传递给函数一样,在 Haskell 中,您可以将一个函数传递给另一个函数。在有限范围内,可以用诸如 C 语言等语言中的函数指针来这样做,但是 Haskell 的用途更为广泛。
Haskell 的第一类函数的能力很大程度上存在于 Haskell 的类型检查系统。在 C 中,可能编写一个“快速排序”函数,将函数指针作为自变量接受,很象 Haskell 示例。但是,在 C 中,您很难确保(所指向的)函数有正确的类型说明。即,无论哪个函数作为自变量传递给 qsortF,都必须取两个同一类型的自变量(“a”代表一般类型)并产生一个 Bool 结果。当然,作为第二个自变量传递给 qsortF 的列表也必须是同一类型“a”。还要注意,样本代码中给定的类型说明只是文档编制所需要的。如果省去说明,则 Haskell 会自动推断所有这些类型的约束。tailComp 满足正确的类型说明,其类型为 String,这是 qsortF 自变量中允许的特殊化的常规类型(不同的比较函数可能对不同的类型或类型类进行操作)。
– Quick sort algorithm with arbitrary comparison function
qsortF :: (a -> a -> Bool) -> [a] -> [a]
qsortF f [] = []
qsortF f (x:xs) = qsortF f [y | y<-xs, f y x] ++
[x] ++
qsortF f [y | y<-xs, not (f y x)]
– Comparison func that alphabetizes from last letter back
tailComp :: String -> String -> Bool
tailComp s t = reverse s < reverse t
– List of sample words
myWords = [”foo”, “bar”, “baz”, “fubar”, “bat”]
– tOrd is [”foo”,”bar”,”fubar”,”bat”,”baz”]
tOrd = qsortF tailComp myWords
– hOrd is [”bar”,”bat”,”baz”,”foo”,”fubar”]
hOrd = qsortF (<) myWords
第一类函数(函数工厂)
将函数传递给其它函数只是第一类函数的部分功能。函数也可以当作工厂,并由此产生一些新函数。在程序工具中创建具有任意功能的函数的能力可以相当强大。例如,可以通过计算产生一个新的比较函数,接着将它传递给前一屏中的 qsortF 函数。
一般,创建函数的方法是使用 lambda 记号。具有函数型特性的许多语言都将字“lambda”用作运算符的名称,但是 Haskell 使用反斜杠字符(因为它优点类似于希腊字母 lambda)。lambda 记号非常象类型说明。箭头表明 lambda 记号描述从一种事物(反斜杠后的事物)到另一种事物(在箭头后的事物)的函数。
示例工厂 mkFunc 将一定量的描述压缩成一个简短描述。主要需注意的是 lambda 表明从 n 到结果的函数。通过类型说明,虽然类型推断允许一个更宽的类型,但每一个都是 Int。函数定义的格式是原语递归。一个空列表产生的结果为零。非空列表产生的结果由它的 head 对提供或是如果只考虑它的末尾(并且末尾最终通过递归趋于空)而产生的结果。
– Make an “adder” from an Int
mkAdder n = addN where addN m = n+m
add7 = mkAdder 7 — e.g. ‘add7 3′ is 10
– Make a function from a mapping; first item in pair maps
– to second item in pair, all other integers map to zero
mkFunc :: [(Int,Int)] -> (Int -> Int)
mkFunc [] = (n -> 0)
mkFunc ((i,j):ps) = (n -> if n==i then j else (mkFunc ps) n)
f = mkFunc [(1,4),(2,3),(5,7)]
– Hugs session:
– Main> f 1
– 4 :: Int
– Main> f 3
– 0 :: Int
– Main> f 5
– 7 :: Int
– Main> f 37
– 0 :: Int
基本语法
本教程至此,我们以非正式方法已看了一些 Haskell 代码。在最后一部分中,我们将明确一些我们已在做的内容。事实上,Haskell 的语法非常直观,而且易懂。通常,最简单的规则就是“写下您的意思”。
Haskell和文字Haskell
本教程中的示例都使用了标准 Haskell 格式。在标准格式中,在注释的左边用双破折号表示注释。示例中的所有注释都是行尾注释,来表示行中双破折号后的所有内容都是注释。也可以用一对“{-”和“-}”将块括起来,创建多行注释。标准 Haskell 文件应该用 .hs 扩展名命名。
文字脚本编制是 Haskell 源文件的一种替代格式。在以 .lhs 扩展名命名的文件中,所有程序行都以大于字符开始。非程序行的内容就是注释。这种样式强调对程序的描述甚于程序的实现。它类似于:
Factorial by primitive recursion on decreasing num
> fac1 :: Int -> Int
> fac1 n = if n==1 then 1 else (n * fac1 (n-1))
Make an “adder” from an Int
> mkAdder n = addN where addN m = n+m
> add7 = mkAdder 7
越位规则
有时,在 Haskell 程序中,函数定义将跨多行,并由多个元素组成。同一概念级别上的元素块的规则是它们应该缩进相同的量。属于更高级别元素的元素应该缩进得更多。只要发生凸出,则提示更深的行都倒退一个概念级。实际上,这是显而易见的,Haskell 几乎会一直报错。
– Is a function monotonic over Ints up to n?
isMonotonic f n
= mapping == qsort mapping — Is range list the same sorted?
where — “where” clause is indented below “=”
mapping = map f range — “where” definition remain at least as
range = [0..n] — indented (more would be OK)
– Iterate a function application n times
iter n f x
| n == 0 = x — Guards are indented below func name
| otherwise = f (iter (n-1) f x)
我发现两个空格的缩排对子元素的外观比较好看,但是为了可读性,在格式编排中可以有许多自由性(只是在同一级别中不要凸出)。
运算符和函数优先级
Haskell 中的运算符分为多个优先级。大多数优先级与您从其它编程语言期望的相同。乘法比加法的优先级高,等等(所以“2*3+4”为 10,不是 14)。Haskell 的标准文档可以提供详细信息。
但是,Haskell 优先级中有一个“gotcha”很容易出错。函数比运算符的优先级高。结果是,表达式“f g 5”表示“将 g(和 5)作为 f 的自变量应用”,而不是“将 (g 5) 的结果应用于 f”。更多的时候,这种错误会产生编译器错误消息,因为(例如)f 会需要一个 Int 作为自变量,而不是另一个函数。但是,有时情况可能更糟,并且您能编写有效内容,但是是错误的:
double n = n*2
res1 = double 5^2 — ‘res1′ is 100, i.e. (5*2)^2
res2 = double (5^2) — ‘res2′ is 50, i.e. (5^2)*2
res3 = double double 5 — Causes a compile-time error
res4 = double (double 5) — ‘res4 is 20, i.e. (5*2)*2
象其它语言一样,括号在消除表达式歧义时非常有用,当您有对优先级有点疑惑时(或只是要明确记录意图时)。顺便说一下,注意:括号不用于 Haskell 中的函数自变量;但是对它们进行伪装没有什么危害,只是创建一个额外表达式分组(如上面的 res2)。
名称的作用域
您可能想到本教程中的两点之间会有冲突。一方面,我们已经说过仅在程序中把名称定义为表达式一次;另一方面,许多示例都重复使用相同的变量名。这两点都是真的,但是需要优化。
每个名称实际只在给定作用域内定义一次。每个函数定义定义它自己的作用域,而且定义内的一些结构定义它们自己更窄的作用域。幸运地是,定义子元素的“越位规则”也精确定义了变量作用域。一个变量(实际是名称)只能在给定的缩排块操作中出现一次。让我们看一个示例,它与前面的示例很相似:
x x y — ‘x’ as arg is in different scope than func name
| y==1 = y*x*z — ‘y’ from arg scope, but ‘x’ from ‘where’ scope
| otherwise = x*x — ‘x’ comes from ‘where’ scope
where
x = 12 — define ‘x’ within the guards
z = 5 — define ‘z’ within the guards
n1 = x 1 2 — ‘n1′ is 144 (’x’ is the function name)
n2 = x 33 1 — ‘n2′ is 60 (’x’ is the function name)
无需多说,这个示例引起不必要的混淆。但是,它是可以理解的,特别是因为自变量在特定函数定义内只有一个作用域(而且在其它函数定义中可以使用相同的名称)。
分解问题
您可能已注意到一点:Haskell 中的函数定义与其它语言相比较,非常的简短。Haskell 语法简练是其原因之一,但是更重要的原因是在函数型编程中强调将问题分解成它们的多个组件部分(而不是象命令式程序中只是在每个点上“执行需要执行的内容”)。这增强了各部分的可重用性,并允许更好地验证每个部分实际执行了希望它所做的内容。
可以用多种方法分离出函数定义的各个小部分。一种方法是在源文件中定义多个有用的支持函数,并按需要使用它们。本教程中的示例主要都是这样做的。但是,还有两种(等价的)方法在单个函数定义的狭小作用域中定义支持函数:let 子句和 where 子句。下面是一个简单示例。
f n = n+n*n
f2 n
= let sq = n*n
in sq+n
f3 n
= sq+n
where sq = n*n
这三个定义是等价的,但是 f2 和 f3 选择在函数作用域中定义一个(琐碎的)支持函数 sq。
导入/导出
Haskell 还支持一个模块系统,允许用于较大规模的函数模块性(还可用于本介绍性教程中未涉及的类型)。模块控制的两个基本元素是导入规范和导出规范。前者与 import 说明一起使用;后者与 module 声明一起使用。有些示例包括:
– declare the current module, and export only the objects listed
module MyNumeric ( isPrime, factorial, primes, sumSquares ) where
import MyStrings — import everything MyStrings has to offer
— import only listed functions from MyLists
import MyLists ( quicksort, findMax, satisfice )
— import everything in MyTrees EXCEPT normalize
import MyTrees hiding ( normalize )
— import MyTuples as qualified names, e.g.
— three = MyTuples.third (1,2,3,4,5,6)
import qualified MyTuples
您会发现 Haskell 对其它函数可以看见函数定义的位置提供了相当多的细颗粒度的控制。这一模块系统帮助构建大规模组件化的系统。
参考资料
要获取 Haskell,请访问 Haskell.org。Haskell 有几个用于多平台的实现。它们包括一个名为 Hugs 的解释型版本和几个 Haskell 编译器。 最近有两本关于 Haskell 新书可以帮助您更好地学习: Haskell: The Craft of Functional Programming (Second Edition),由 Simon Thompson 著(Addison-Wesley,1999) The Haskell School of Expression: Learning Functional Programming through Multimedia ,由 Paul Hudak 著(Cambridge University Press,2000)
更多阅读
《精神分析入门》摘要 精神分析入门读后感
《精神分析入门》摘要第一章1、精神分析理论的两个假设:心理决定论(因果)原理;潜意识的作用论。2、决定论:我们生活中看似偶然的事情,好像与过去事没有关系,其实不然,经精神分析家75年对“偶然”或“碰巧”的研究(由弗洛伊德本人开始),表明
AI基础教程入门一 ai基础教程入门视频
第一课(1-3节)一、软件介绍:Illustrator英译:插画它集图形设计、文字编辑及高品质输出于一体的矢量图形软件,广泛应用于平面广告设计、网页图形制作、插画制作及艺术效果处理等诸多领域。基本术语和概念:
漂移板启动新人入门技巧 漂移板基础入门花式
新人必看的漂移板入门技巧刚拿到自己喜欢的漂移板,是不是很着急把他学会啊?人都说“心急吃不了热豆腐”,此话一点都没有说错,慢慢来什么都会有好的结果
股票入门书籍大全 股票投资入门书籍
股票知识理论书籍《股票基础知识》《新股民实战必读全书》《新股民实战操作大全》《从炒股新手到高手》《炒股就这几招》《炒股入门:小夏入市记》《轻轻松松炒股票》《股票投资风险防范》《42种股票投资方法策略》《选股致胜技巧》
佳能A650IS新手入门 象棋怎么玩新手入门
现在A650 IS的队伍越来越大了,不少网友用A650IS拍摄出很多漂亮的摄影作品。但也发现个别网友还是对A650IS的某些功能不尽了解。为此利用今天的闲假时间写出帖子,想和大家交流一下使用A650 IS的体会和经验。 说实话,本人之前虽然用过