懂一点Haskell(二)

函数式编程基础

编程有两种根本不同的方式,顺序式和函数式。顺序式最好的例子是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

接下来就进入主题了,把函数作为参数!把函数作为参数,我们就可以把上面两个函数ifEvenIncifEvenDouble改为一个函数ifEven,然后把incdouble函数以参数方式传进去。

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

你可能感兴趣的:(懂一点Haskell(二))