函数式思维的动力来自数学。数学函数有很多特色——函数式语言试图模拟真实世界。
所以一开始,我们以一个加1函数
开始:
Add1(x) = x+1
这意思是什么?好吧,看起来十分直白。它意味着有一个操作以一个数字开始,然后给它加1。
我们引入一些术语:
- 可以被函数作为输入的值的集合叫做domain。这样,它可能是实数集合,为了简单,我们仅限于整数。
- 可以被函数作为输出的值的集合叫做range(更科学的应该是叫作codomain的image)。还是仅限于整数。
- 函数被称作映射domain到range。
在F#中,我们这样定义:
let add1 x = x + 1
如果你在F#交互窗口输入,你可以看到结果(函数的签名):
val add1 : int -> int
仔细看一下输出:
- 大体意思是:函数add1将domain的整数映射到range中的整数
- add1定义为val,value的简写。我们等一下再讨论value。
- 箭头
->
用来展示domain和range。这里,domain是int类型,range是int类型。
注意类型不是特定的,F#编译器会猜测函数在使用int。
数学函数的关键属性
数学函数有很多属性和你在过程式编程用到的函数所不同。
- 函数对于给的的输入值会给你相同的输出
- 函数没有负面效应
这个数学提供了很多有力的好处,所以函数式编程语言在设计时也是试图加强这些属性。我们反过来挨个看它们。
函数对于给的的输入值会给你相同的输出
在命令式编程,我们认为函数做事情或者计算事情。数学函数没有做任何计算——它纯粹是将输入映射到输出。事实上,另一个方式思考定义一个函数是简单地作为所有映射的集合。例如,以一个非常生硬的方式这样定义add1函数(用C#):
int add1(int input) { switch (input) { case 0: return 1; case 1: return 2; case 2: return 3; case 3: return 4; etc ad infinitum } }
很明显,我们不能为每个可能的整数设置一个case,但是原理是一样的。你可以看到绝对没有一个计算,仅仅是一个遍历。
函数没有负面效应
在数学函数,输入和输出逻辑上是两个不同的东西,它们都是预先定义好的。函数没有改变输入和输出——它仅仅是从domain映射已经存在的输入值到range中的已经存在的输出值。
换句话说,函数没有可能在输入有任何的效应。函数没有实际计算或者操作什么,它仅仅是一个美化了的遍历。
值的不变性是微妙的但是十分重要。如果我在做计算,我不期望数字来改变它,当我将它们相加的时候。比如,如果我有:
x = 5 y = x+1
我不期望加1后,x被改变。我会期望得到一个不同的数字y,同时x没有被碰到。在数学的世界里,整数已经以一个无法改变的集合存在,add1函数仅仅定义了它们之间的关系。
pure函数的能力
这种函数有重复的结果,而且没有负面效应,我们称之为pure函数。你可以用它们做很多有意思的东西:
- 它们是简单地并行的。我可以拿所有1到1000的整数,说,给定1000个不同的CPU,我可以使它们同时在对应的整数值来执行add1函数。这是安全的,因为它们之间没有必要有任何的交互。不需要锁,mutex,semaphore等。
- 我可以懒惰地使用函数,只有当我需要输出地时候才执行它。我可以确定结果都会是一样的,不管我是现在执行还是稍后。
- 我只需要为一个特定的输入执行一个函数,然后缓存结果,因为我知道了相同的输入总是对应相同的输出。
- 如果我有很多pure函数,我可以以我喜欢的方式执行它们。再声明一遍,它不会改变最终结果。
你看到了如果我们可以创建函数式编程语言,我们可以立马得到很多厉害的技术。确实你可以用F#做这些事情。
数学函数的unhelpful属性
输入和输出不能变
理论上不变的值看起来是个好主意,但是如果你不能以传统方式给变量赋值你如何完成工作呢?
我确保你这不像你可能想的那样是个问题。当你看完这个系列,你就会看到实际中是如何解决的。
数学函数常常只有一个输入和输出
你从图表看到,数学函数常常只有一个输出。确实是这样,尽管当你第一次看到它们的时候没有那么明显。
看起来是个很大的烦恼。不用更多参数,如何能做有用的事情?
有一个方式来做,而且,F#中它是对你完全透明的。它叫做“currying”。
事实上,你会在后面发现,这两种“unhelpful”属性会证实是难以置信的有用——也是使得数学函数非常厉害的关键部分。