函数式编程基础
编程有两种根本不同的方式,顺序式和函数式。顺序式最好的例子是C语言,它依赖于一个特定的模型,比如冯诺依曼模型。写C语言程序,你得懂一些计算机基础知识,得自己分配内存......你的每一行程序甚至都能找到对应的计算机指令。而函数式则侧重于从数学角度分析问题。重点关注计算,而不是电脑。相比而言,函数式编程语言的函数更像数学里的函数(接下来会讲Haskell中函数必须遵循的几点准则),而C语言的函数则没有这么严格的要求。Haskell是一门纯函数式编程语言。而更多其它语言则是在这两种方式之间寻求某种折衷。
function
我们从函数式编程中最基础的概念函数开始说起。那么,什么是函数?Haskell中的函数来源于数学中的函数概念。在数学中,当我们定义一个函数 ƒ(x)=y,意思是有一个函数 ƒ,它接受一个参数 x, 映射到值 y 。对于函数 ƒ而言,每一个 x 只能有一个特定的 y 与之对应。Haskell的函数就和数学里的函数一样!
现在我们定义一个函数addThree,它接受三个参数并返回他们的和。
-- 函数名 addThree,参数 x y z,返回 x+y+z
addThree x y z= x + y + z
如上例,定义一个Haskell函数就是这么简单!
Haskell函数必须遵循的三条守则:
- 所有的函数必须接受至少一个参数
- 所有的函数必须返回一个值
- 无论何时以相同参数调用一个函数时,必须保证相同的输出。
第三条规则又叫做 引用透明性(referential transparency) 这里有必要重点说一下引用透明性,Haskell号称可以写出零bug的程序正是因为这一特性。因为你每做一次函数调用都可以保证得到期待的输出!而没有任何副作用(side effect)!
有人就要问了,难道C语言的函数就不能保证确定的输出吗?下面是实现三个数相加的C语言代码。
int addThree(int x, int y, int z){
return x+y+z;
}
就这个代码而言,当然可以保证确定的输出。可是,在C语言中你还可以这样做。
int a = 8;
int change_a(){
++a;
}
上面这个C程序不仅没有参数,而且每次调用你根本不知道它干了什么!你也不知道它对谁进行了什么操作!当然,除非你看源代码。可是我们创建函数不就是为了用吗?难道每个函数都得去查看它的源代码?全局变量和静态变量的存在使程序变得复杂起来,当然,某种程度上也带来了一定的便利。在Haskell中,就没有全局变量和静态变量这一说。甚至变量都不是真正的”变量“,比如 x=2
,变量一旦定义便不能更改其值了,x
在这个程序中永远都会等于2
。你可以把Haskell中的变量理解为“定义”,这样更贴切些。然而,Haskell允许在GHCi中进行变量更改,也算是给大家一个方便。但在.hs
文件中是坚决不允许对变量进行重新赋值的!
Lambda
lambda function,又叫匿名函数。即没有名字的函数。它是函数式编程中基础的概念。以一个例子说明Haskell中匿名函数的定义方法。
-- \ 后是参数,本例x,-> 后是函数体即x的映射。
-- 和它等价的函数是 double x = x * 2
\x -> x * 2
先来使用一下这个匿名函数。打开ghci:
Prelude> (\x -> x * 2) 6
12
Prelude> double 6
12
两个函数完全相同,那么为什么不用有名字的函数呢?事实上,确实推荐使用有名字的函数。lambda一般用于只使用一次的函数,因为只使用一次,也就懒得定义函数了。
first-class function
函数作为参数
假如你有一个函数ifevenInc
,如果参数n
为偶数就给n
加一,否则返回它本身。
ifEvenInc n = if even n
then n + 1
else n
这时你又想写另一个函数ifEvenDouble
,如果参数n
为偶数,就翻倍,否则返回它自身。
ifEvenDouble n = if even n
then n * 2
else n
这两个函数除了then
后面的部分,其它部分完全相同!说不定以后你还会想写ifEvenSquare
,Haskell推荐的做法是把大函数分拆成多个小函数。虽然这个函数并不大,但可以说明这个思路。我们的做法是把then
后面的部分提出来写成函数。
inc n = n + 1
double n = n * 2
接下来就进入主题了,把函数作为参数!把函数作为参数,我们就可以把上面两个函数ifEvenInc
和ifEvenDouble
改为一个函数ifEven
,然后把inc
,double
函数以参数方式传进去。
ifEven func n = if even n
func n
else n
你也可以把匿名函数当参数用。打开ghci:
Prelude> ifEven (\x -> x+1) 6
7
Prelude> ifEven double 8
16
函数作为返回值
以下面的例子说明函数作为返回值的用法及用途。
wuhanOffice name = name ++ ": Box 789 - wuhan, 10013"
shanghaiOffice name = name ++ " Box 456 - shanghai, 89523"
xianOffice name = name ++ " Box 123 - xian, 65535"
假如有以上三个函数,它们分别生成 name 对应不同城市的邮寄地址。
它们这样使用:
*Main> wuhanOffice "Bob"
"Bob: Box 789 - wuhan, 10013"
*Main> xianOffice "Alice"
"Alice Box 123 - xian, 65535"
这几个函数功能很简单,只是为了说明问题。现在我们知道name在哪个城市,所以可以直接调用。但如果我们事先不知道name属于哪个城市,那么该如何选择调用wuhanOffice还是xianOffice呢?我们可以再写一个函数addressLetter,把城市也作为参数和name一起传进去,像下面这样:
addressLetter name city = ...
在函数体里面进行城市的选择,然后执行相关的操作。而这些“相关操作”我们之前已经定义好了函数,因此可以再写一个函数,传入city得到对应的函数。这就是把函数作为返回值。如下:
getLocationFunction city = case city of
"wh" -> wuhanOffice
"sh" -> shanghaiOffice
"dc" -> dcOffice
_ -> (\name -> name)
然后我们的addressLetter就可以这样写了:
addressLetter name city = locationFunction name
where locationFunction = getLocationFunction city