- 浏览: 32675 次
- 性别:
- 来自: 北京
2008年03月27日 星期四 18:23
by rufi 2003.3.21 -- 2003.4.2
一.序
1.什么是Haskell?
Haskell是一种函数编程语言. 1980年代以前对函数编程有很多研究, 但不同的研究者使用各自不同的语法记号, 一起交流时造成一些不便. 后来1987年的时候, 在FPCA'87会议上制定了统一的Haskell语言. Haskell吸收了各家的长处, 是一种纯粹的函数编程语言,并根据科学家Haskell B.Curry的名字命名. Haskell经过多年的发展完善, 目前使用的版本是Haskell 98.
2.Haskell有什么特点?
相对Haskell来说,传统的Basic,Pascal,C++,C#,Java,Python等都是命令(imperative)编程语言, 程序语句有一定的执行次序. 函数(functional)编程语言则给出执行的内容, 关注于更高层次的"做什么"而不是"怎么做", 这就是二者最明显的一个区别。函数编程语言的语法功能非常强,使编程的效率大幅提高。
Haskell是世界上公认的语法最优美最简洁的一种语言。的确,Haskell语言是写给人看的,而不是写给机器看的。另一方面,这也使得的 Haskell的编译技术成为一个难点。从以人为本的角度来看,程序员的时间比机器的时间更宝贵,所以Haskell是明智的选择。
3.如何获得Haskell?
Haskell是一个公共的语言定义, 任何人都可以编写它的实现(implementation), 因而Haskell有很多解释器(比如Hugs)和编译器(比如GHC), 它们都可以在www.haskell.org上得到. 解释器的优点是便于学习和开发,程序写好后不需要编译直接就可以运行,编译器则可以将程序编译可独立执行的文件,便于发布. Haskell既能解释执行, 也能槐槐嘁? 这也是优于其他语言的一个地方.
附:Hugs使用指南
本文中的示例程序都将在Hugs中运行, 在这里简要介绍一下Hugs的使用方法。Hugs可以在http://www.haskell.org/hugs/下载,安装文件只有2.8M, 是学Haskell的必备工具.
使用方法:
1.用你自己喜欢的文本编辑器将源程序写好, 保存到文件中, 文件以扩展名 hs 结尾.
2.运行Hugs, 出现提示符: Prelude> ,表示Prelude库已经装载.
3.输入:? 可以查看可供使用的一些命令的说明
4. 先输入:!, 然后就可以输入DOS命令并执行. 比如输入:!dir查看当前的工作目录
5. 输入:cd directory_name 将工作目录改为你保存源文件的目录
6. 输入:l file_name 将源程序载入, 提示符变为Main>
现在就可以在提示符后输入各种表达式以检验所定义的函数的功能, 执行所需的运算.
注意: 在提示符后不可以定义新的函数, 因为Haskell中各语句不是顺序执行的, 而把整个源文件当作一个整体来处理, 在编辑器中修改源程序并保存后, 只要输入:r就重新载入, 改动就生效了.
二.语法概要
1.注释有两种: 一种以"--"开始到行尾结束, 一种以"{-"开始,以"-}"结束,可以延续多行.
2.表达式和函数都有类型,但类型声明不是必需的,有一套类型推断系统可以推断出所涉及的类型.
3.函数的定义多使用模式(pattern)匹配的方法, 两个标识符(identifier)邻接就表示函数调用.
4.函数变量和类型变量的标识符以小写字母开头, 类型构造和模块的标识符以大写字母开头.
5.语句块以缩进区分, 从同一列开始的语句属于同一个块.
6.保留字: case class data default deriving do else if import in infix infixl infixr instance let module newtype of then type where _
7.运算符可以自定义,由以下符号进行组合:
: # $ % & * + - = . / \ < > ? ! @ ^ |
预定义的运算符有:
+ - * / ^ $ ++ . && || == /= <= >= < >
: // = @ -> => .. :: <- !!
8.数字: 123 12.3 1.23e5 0o67 0x3A
字符: 'a' 'A' '\n'
字符串: "character string"
三.常数
在函数编程语言中, 函数是一个中心概念, 常数也可看作是常函数. 如下语句:
f = 3
定义了一个常数. f 被定义为3后就不能重复定义 f=5 ,任何时候调用f, 它都返回3. 这就消除了一些边际效应,减少了出错的可能性. 以下代码在函数编程和命令编程中具有完全不同的含义:
a = 2
b = 4
c = a + b
在函数编程中解释为: 定义常数a为2, 定义b为4, 定义c为a,b之和.
在命令编程中解释为: 给变量a赋值2, 给b赋值4, 求a,b之和赋给c.
定义和赋值的区别在于, 定义不分次序, 赋值有次序, 以上程序在Haskell中完全可以倒过来写:
c = a + b
a = 2
b = 4
另外, 定义并不计算, 比如:
d = 3*5
e = 1/0
在命令程序中, e=1/0会引发一个错误, 在Haskell中只有当计算e时才引发错误.
Haskell的思维更像是人脑的思维而不是机器的思维.
也可以给常数加以类型说明, 比如:
b :: Int
如果没有这一类型说明, 系统自动根据 b=4 推断b的类型为: Integer
Int和Integer区别是Int的取值范围是有限的, Integer的大小是无限的. 因为Integer比Int更普遍,所以在没有明显说明b的类型为Int时, 就自动推断b的类型为: Integer
再举几个例子,在Haskell标准库Prelude中定义的常数中,有两个是:
pi = 4 * atan 1
otherwise = True
四.单变量函数
如下语句:
f (x)=x+2
double(x)=2*x
就定义了两个简单的函数, 函数的类型由自变量的类型和返回值在类型决定, 用运算符"->"连接.
比如可以用以下语句说明它们的类型:
f :: Int -> Int
double :: Float -> Float
表示f是从Int变量到Int变量的映射. 如果没有明显说明, 系统根据函数定义中所涉及到的运算(+),(*)推断这两个函数的类型为:
Num a => a -> a
这里a是一个类型变量, 属于一个独立的名字空间, Num是一个类, Num a => 表示类型a属于Num类. Num类中定义了(+)和(*)的运算, 继承此类的类型也支持这两种运算. 这样使用类来限定函数的类型, 使函数具有的普遍性. 把类型归为一些类, 实现了类型的多态, 也简化了编程的任务.
在Haskell中函数非常频繁地用到, 通常在函数的定义和使用中省去括号, 直接用两个标识符邻接表示函数作用. 所以f和double的定义可写为:
f x = x + 2
double x = 2 * x
调用函数的格式和定义函数的格式基本是相同的, 比如定义函数g如下:
g x = 2 * f x
函数作用的优先极高于其他所有的运算符, 所以2 * f x等价于2 * (f x), 也等价于2 * (f(x)).
函数作用的次序是从左向右的, 所以可以等价地定义g为:
g x = double (f x)
Prelude有一个运算符$的定义如下:
infixr 0 $
($) :: (a -> b) -> a -> b
f $ x = f x
可见, $也是表示函数作用的, 但它的优先级最低, 而且作用次序是从右向左的.所以还可以等价地定义g为:
g x = double $ f x
引入$, 避免了括号的使用, 尤其当$后的表达式很长有多行时使用$是很方便的.
五. 多变量函数
严格说来, 一个函数只能接收一个参数, 返回一个值. 但有两种方法可以实现多变量函数.
1.Curried函数
函数接受一个参数后, 返回的也是一个函数, 这个函数对可以接受别的参数. 比如:
add :: Int -> Int -> Int
add x y = x + y
从add类型可以看出, 有两个"->", 而"->"的结合次序是从右向左, 所以add的类型是:
Int -> ( Int -> Int )
即add接受一个Int参数, 返回一个( Int -> Int )的函数, 这个函数再接受一个Int返回一个Int.
这样的函数就叫curried函数.
2.使用数组
数组就是把几个变量放到一起看成是一个变量, 从而使函数可以输入和输出多个变量. 比如:
add :: (Int,Int) -> Int
add (x,y) = x+y
数组是Haskell中的一种数据结构, 数组中可以放不同类型的变量, 数目不限但长度固定, 数组的类型就是数组内各元素的类型组合起来构成一种类型.
这两种函数在使用中各有特色, 而且可以用Prelude中定义的curry和uncurry互相转换.
curry :: ((a,b) -> c) -> (a -> b -> c)
curry f x y = f (x,y)
uncurry :: (a -> b -> c) -> ((a,b) -> c)
uncurry f p = f (fst p) (snd p)
稍作一点解释:类型说明中出现的小写a,b,c等叫类型变量, 表示任意一种类型. f (x,y)表示f接受数组为参数, curry f x y就是(curry f) x y 表示curry f可以接受两个变量为参数. 令 g=curry f, 则 g x y = f (x,y). 可见curry的转换作用, curry的类型表达更清楚和说明了这一点. uncurry也是一样的道理, 其中的fst和snd分别表示取二元数组的第一个和第二个元素.定义如下:
fst :: (a,b) -> a
fst (x,_) = x
snd :: (a,b) -> b
snd (_,y) = y
"_"叫匿名变量, 匹配指定类型任意的输入值, 该值在"="后的表达式中不会用到.
六. 离散函数
有些函数的参变量只有有限个取值, 比如Prelude中not的定义如下:
not :: Bool -> Bool
not True = False
not False = True
not对逻辑变量取反.
离散的变量可以使用保留字data定义, 比如:
data Direction = Left|Up|Right|Down
这就定义了一种Direction类型的变量, 这种变量的取值只有四个值:Left,Up,Right,Down.
data定义了一个新的类型, type则可以给一个已有的类型取一个别名.比如:
type Point = (Float, Float)
(Float, Float)是容纳两个Float变量的数组的类型, 取别名后既简化了书写, 也附加了含义.
现在看针对离散变量的函数如何定义:
move :: Direction -> Point -> Point
move Left (x,y) = (x-1,y)
move Up (x,y) = (x,y-1)
move Right (x,y) = (x+1,y)
move Down (x,y) = (x,y+1)
即分别对应离散变量的每一个取值对函数作相应的定义. 以(x,y)表示位置, 给move输入移动的方向和当前的位置, 就输出了移动后的位置.
再看一个例子:
data Thing = Paper | Stone | Scissors deriving Show
beat :: Thing -> Thing
beat Paper = Scissors
beat Stone = Paper
beat Scissors = Stone
定义Thing类型有三个取值, deriving Show表示Thing类型继承Show类, 从而拥有的Show的method, Show类的作用是将取值转化为字符串在屏幕上显示. beat定义了三种物品,纸,石头,剪刀之间的输赢关系.
自然数是离散的也是无限的, 以自然数为变量的函数通常用迭代的方法定义, 即函数自己调用自己.举个例子:
fac 0 = 1
fac n = n * fac (n-1)
这样计算fac n时, 先去计算fac (n-1),有fac n = n * fac (n-1)=n * (n-1) * fac (n-2),如此类推, 一直算到fac 0, 最后的结果是把从1到n的自然数全部连乘起来.
再比如:
increment,fib :: Int -> Int
increment x=x+1
fib 1 = 1
fib 2 = 2
fib n = fib (n-1) + fib (n-2)
当计算函数时, 按照输入的参数逐一匹配所给的模式, 如果无法匹配时给出错误中断. 对于自然数模式匹配还允许用如下方式定义函数:
foo (n+1) = n-1
Haskell中预定义的针对字符的函数有:
isAscii, isLatin1, isControl, isPrint, isSpace, isUpper, isLower,
isAlpha, isDigit, isOctDigit, isHexDigit, isAlphaNum,
digitToInt, intToDigit,
toUpper, toLower,
ord, chr,等
ord将字母转换为数字, chr反之.
七. 连续函数
Haskell中整数可以用Int和Integer表示, 实数可以用Float(单精度)和Double(双精度)来表示. 有理数还可用Rational表示, 相当于无限精度的浮点数.
Prelude中定义了两个在数学上较基本的函数:
1. 常数函数
const :: a -> b -> a
const k _ = k
2. 单位函数
id :: a -> a
id x = x
当函数针对变量的不同取值范围有不同的行为时, 就要用到选择结构. 比如:
3. 绝对值函数
abs' x = if x>=0 then x else -x
if ... then ... else ...的结构也可以用"|"(guard)来表达, 上述abs'也可写成:
abs' x |x>=0 = x
|otherwise = -x
"|"还可用于更多的分支选择, 比如:
4. 符号函数
sign x |x>0 = 1
|x==0 = 0
|x<0 = -1
绝对值函数和符号函数在Prelude中分别为abs和signum, 其定义是用prime函数实现的.
下面再举几例子,以熟悉函数的定义方法.
5. 二次方程求根函数:
root (a,b,c) = (x1,x2) where
x1=(-b+d)/(2*a)
x2=(-b-d)/(2*a)
dd = b*b-4*a*c
d | dd>=0 =sqrt dd
| dd<0 =error "No real root"
这里where引入一个内嵌的语句块, 在其中定义的函数在外部是看不到的. error函数用来提示出错的信息.
6. 求导函数
diff :: (Float->Float) -> (Float->Float)
diff f = f’
where f’ x = (f (x+h) - f x) / h
h = 0.0001
为了h的取值可调, 也可以把h包括在参数中:
flexDiff h f x = (f(x+h)-f(x))/h
把flexDiff h sin 1的取值和cos 1的取值在h=0.001,0.0001,0.00001下比较, 取0.0001的接近程度最好.
7. 方程求根
利用牛顿法可定义函数为:
zero :: (Float->Float) -> Float
zero f = until goodEnough improve 1.0
where improve b = b - f b / diff f b
goodEnough b = f b ~= 0.0
其中until是Prelude中定义的执行循环的一个函数, 其定义如下:
until :: (a -> Bool) -> (a -> a) -> a -> a
until p f x = if p x then x else until p f (f x)
until的作用是看条件p x是否成立, 不成立就用f作用x, 返回值替代原来的x, 直到条件p x成立为止.
表示约等于的运算符~=则需要自己定义.
8. 求逆函数
利用方程求根的结果就可以求逆:
inverse :: (Float->Float) -> Float -> Float
inverse g a = zero f
where f x = g x - a
八. 数列和数组
数列中元素的类型一致, 元素数目可变; 数组中元素的类型任意, 元素数目固定. 可以说数列和数组对数据的抽象做到了性能与灵活性的恰到好处, 有些语言中只提供一种容器, 元素数目可变, 类型也任意, 其结果是无法满足类型完全的需要, 也将低了运算的效率.
数组的使用比较简单, 对于数列来说则大有文章.
1. 数列的构造
数列是用[]和(:)构造的, []是一个空的数列, x:xs的含义是元素x附加到数列xs的前面组成一个更长的数列. 比如, 1:[] 等于[1], 2:3:1:[]等于[2,3,1], 运算符(:)是从右向左运算的. 所有的数列都可以看作是从[]开始, 将各元素用(:)附到上面形成的. 在实际编程中有一些简记法可以快速地构造数列.
a. 列举法
将数列的元素一一列举, 比如: [1,2,3], ['A','B','d'], [[1,2], [4,5,6]]等等, 数列的类型用"[元素类型]"来表示, 这几个例子的类型依次为: [Int], [Char], [[Int]].
b. 给出变化范围
适用于构造等差数列, 比如: [1..5]等于[1,2,3,4,5], ['a'..'d']等于['a','b','c','d']等于"abcd"因为type String=[Char]. 默认的等差为1, 也可以给出前两个元素指定等差, 比如: [2,4..8]等于[2,4,6,8], [2,4..7]等于[2,4,6], [2.5,4..9.5]等于[2.5,4.0,5.5,7.0,8.5,10.0].
c. 描述法
描述法给出数列中元素取值的范围以及所满足的条件, 与数学中集合的描述法是一样的. 例如:
[3*x+2| x<-[3,6,9]] --记号"<-"表示属于,x依次取3,6,9,代入3*x+2,得到数列[11,20,29]
[x*x| x<-[1..9], x `rem` 3==1] --给出x的范围,还限定x除3余1
[(x,y)|x<-[1,2,3],y<-[x..3]] --等于 [(1,1),(1,2),(1,3),(2,2),(2,3),(3,3)]
2. 从数列中选取元素
head --数列的第一个元素
tail --除去第一个元素剩余的数列
last --最后一个元素
init --除去最后一个元素剩余的数列
take n --数列前n个元素
drop n --除去数列前n个元素剩余的数列
(!!)n --第n个元素, 索引指标从0开始
3. 从数列中筛选元素
filter p --把数列中所有满足条件p的元素取出来组成一个数列
takeWhile p --从第0个元素起, 取满足p的元素, 遇到不满足条件的元素时停止
dropWhile p --从第0个元素起, 去掉满足p的元素, 遇到不满足条件的元素时停止
条件p是一个以数列元素为自变量, 返回值为逻辑值的函数. 比如预定义的even,odd判断奇偶性.
4. 常用数列函数
length lst --求数列的元素数目
reverse lst --将数列中元素次序反过来
lst1 ++ lst2 --将两个数列连成一个数列
concat lst --lst是数列的数列,concat将各子数列一个数列
sum lst --对lst中的元素求和
product lst --lst中的元素连乘
elem e lst --判断e是否为lst中的元素
or lst --对类型为[Bool]的lst中所有元素求或
and lst --对类型为[Bool]的lst中所有元素求与
zip
相关推荐
**Haskell教程(中文版)** Haskell是一种纯函数式编程语言,以其强大的类型系统、惰性求值和高阶函数特性而闻名。这本由Hal Daumé III编著并由乔海燕翻译的《Yet Another Haskell Tutorial》中文版,为初学者提供了...
Haskell是一种纯函数式编程语言,它以其独特的...总的来说,Haskell教程会逐步引导初学者进入这个富有挑战性和乐趣的编程世界,通过学习纯函数、惰性求值、类型系统、Monads等概念,他们将能够编写出高效、优雅的代码。
这份Haskell教程的版权归属于Hal Daume III,原作者在2002年至2006年间创作了这份材料,并且给予了在GNU自由文档许可证(GNU Free Documentation License)的条款下分发的权利。该许可证允许人们将作品纳入如...
Haskell是一种标准化的通用纯函数式编程语言,它的设计基于数学原理,特别是λ演算。作为一门纯粹的函数式编程语言,Haskell强调函数是一等公民("函数是第一类对象"),其中主要的控制结构是函数。Haskell语言的...
TutorialsPoint Haskell 教程.epub
《Haskell语言教程》是一本深受开发者欢迎的在线书籍,主要目标是帮助初学者深入理解Haskell这门纯函数式编程语言。Haskell以其强大的理论基础、严格的类型系统和静态类型而著称,它鼓励程序员编写简洁、清晰且易于...
Haskell简明教程 关于functional programming
**Haskell 编程入门五星教程** Haskell 是一种纯函数式编程语言,以其严谨的数学基础、静态类型系统和惰性求值策略而闻名。它鼓励程序员采用声明式编程风格,强调数据流和计算的表达,而不是指令的执行顺序。本教程...
**Haskell 中文教程概述** Haskell 是一种纯函数式编程语言,以其强大的类型系统、惰性求值和静态类型而闻名。它鼓励编写简洁、可读性强且易于维护的代码。本教程将深入探讨 Haskell 的核心概念和语法,帮助初学者...
总之,《Haskell Cookbook》是一本全面且深入的Haskell教程,涵盖了从入门到进阶的各个层面。通过阅读和实践书中的例子,读者不仅能掌握Haskell的基本语法和特性,还能提升在函数式编程领域的思维能力和问题解决技巧...
尽管网上的Haskell教程很多,但每个教程解决问题的思路不同,通过综合阅读可以更好地整理和理解知识碎片。对于初学者,特别是那些没有函数式编程经验的人,可以借助本教程逐步入门。Haskell社区非常友好、耐心,新人...
《Real World Haskell》是一本广泛认可的Haskell编程语言教程,旨在将这门函数式编程语言的理论与实践相结合,让读者能够在实际项目中运用Haskell。这本书的PDF版本是根据2015年3月1日的在线文档转制而成,确保了...
### Haskell文档知识点解析 ...: 一本面向初学者的Haskell教程,风格幽默,内容详尽。 以上内容概括了Haskell文档中的主要知识点,希望能帮助读者更好地理解Haskell语言的特点及其在实际开发中的应用。