This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,318 @@
# 序
现在很多的文章和演讲都在谈架构,很少有人再会谈及编程范式。然而, 这些基础性和本质性的话题,却是非常非常重要的。
一方面,我发现在一些语言争论上,有很多人对编程语言的认识其实并不深;另一方面,通过编程语言的范式,我们不但可以知道整个编程语言的发展史,而且还能提高自己的编程技能,写出更好的代码。
**我希望通过一系列的文章带大家漫游一下各式各样的编程范式。**(这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。)
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)
这一经历可能有些漫长,途中也会有各式各样的语言的代码。但是我保证这一历程对于一个程序员来说是非常有价值的,因为你不但可以对主流编程语言的一些特性有所了解,而且当我们到达终点的时候,你还能了解到编程的本质是什么。
这一系列文章中有各种语言的代码其中有C、C++、Python、Java、Scheme、Go、JavaScript、Prolog等。所以如果要能跟上本文的前因后果你要对这几门比较主流的语言多少有些了解。
而且你需要在一线编写一段时间大概5年以上吧的代码可能才能体会到这一系列文章的内涵。
我根据每篇文章中所讲述的内容,将这一系列文章分为四个部分。
<li>
**第一部分:泛型编程**第1~3章讨论了从C到C++的泛型编程方法,并系统地总结了编程语言中的类型系统和泛型编程的本质。
</li>
<li>
**第二部分:函数式编程**第4章和第5章讲述了函数式编程用到的技术及其思维方式并通过Python和Go修饰器的例子展示了函数式编程下的代码扩展能力以及函数的相互和随意拼装带来的好处。
</li>
<li>
**第三部分:面向对象编程**第6~8章讲述与传统的编程思想的相反之处面向对象设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象列举了面向对象编程的优缺点基于原型的编程范式以及Go语言的委托模式。
</li>
<li>
**第四部分:编程本质和逻辑编程**第9~11章先探讨了编程的本质逻辑部分才是真正有意义的控制部分只能影响逻辑部分的效率然后结合Prolog语言介绍了逻辑编程范式最后对程序世界里的编程范式进行了总结对比了它们之间的不同。
</li>
我会以每部分为一个发布单元,将这些文章陆续发表在专栏中。如果在编程范式方面,你有其他感兴趣的主题,欢迎留言给我。
下面我们来说说什么是编程范式。编程范式的英语是Programming Paradigm范即模范之意范式即模式、方法是一类典型的编程风格是指从事软件工程的一类典型的风格可以对照“方法学”一词
编程语言发展到今天,出现了好多不同的代码编写方式,但不同的方式解决的都是同一个问题,那就是如何写出更为通用、更具可重用性的代码或模块。
如果你准备好了,就和我一起来吧。
# 先从C语言开始
为了讲清楚这个问题我需要从C语言开始讲起。因为C语言历史悠久而几乎现在看到的所有编程语言都是以C语言为基础来拓展的不管是C++、Java、C#、Go、Python、PHP、Perl、JavaScript、Lua还是Shell。
自C语言问世40多年以来其影响了太多太多的编程语言到现在还一直被广泛使用不得不佩服它的生命力。但是我们也要清楚地知道大多数C Like编程语言其实都是在改善C语言带来的问题。
那C语言有哪些特性呢我简单来总结下
<li>
C语言是一个静态弱类型语言在使用变量时需要声明变量类型但是类型间可以有隐式转换
</li>
<li>
不同的变量类型可以用结构体struct组合在一起以此来声明新的数据类型
</li>
<li>
C语言可以用 `typedef` 关键字来定义类型的别名,以此来达到变量类型的抽象;
</li>
<li>
C语言是一个有结构化程序设计、具有变量作用域以及递归功能的过程式语言
</li>
<li>
C语言传递参数一般是以值传递也可以传递指针
</li>
<li>
通过指针C语言可以容易地对内存进行低级控制然而这加大了编程复杂度
</li>
<li>
编译预处理让C语言的编译更具有弹性比如跨平台。
</li>
C语言的这些特性可以让程序员在微观层面写出非常精细和精确的编程操作让程序员可以在底层和系统细节上非常自由、灵活和精准地控制代码。
然而在代码组织和功能编程上C语言的上述特性却不那么美妙了。
## 从C语言的一个简单例子说起
我们从C语言最简单的交换两个变量的swap函数说起参看下面的代码
```
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
```
你可以想一想这里为什么要传指针这里是C语言指针因为如果你不用指针的话那么参数变成传值即函数的形参是调用实参的一个拷贝函数里面对形参的修改无法影响实参的结果。为了要达到调用完函数后实参内容的交换必须要把实参的地址传递进来也就是传指针。这样在函数里面做交换实际变量的值也被交换了。
然而这个函数最大的问题就是它只能给int值用这个世界上还有很多类型包括double、float这就是静态语言最糟糕的一个问题。
## 数据类型与现实世界的类比
与现实世界类比一下,数据类型就好像螺帽一样,有多种接口方式:平口的、十字的、六角的等,而螺丝刀就像是函数,或是用来操作这些螺丝的算法或代码。我们发现,这些不同类型的螺帽(数据类型),需要我们为之适配一堆不同的螺丝刀。
而且它们还有不同的尺寸尺寸就代表它是单字节的还是多字节的比如整型的int、long浮点数的float和double这样复杂度一下就提高了最终导致电工程序员工作的时候需要带下图这样的一堆工具。
<img src="https://static001.geekbang.org/resource/image/4a/3e/4a5e3c03a3aef6015cc93f5f11f8003e.png" alt="" />
这就是类型为编程带来的问题。要解决这个问题,我们还是来看一下现实世界。
你应该见过下面图片中的这种经过优化的螺丝刀上面手柄是一样的拧螺丝的动作也是一样的只是接口不一样。每次我看到这张图片的时候就在想这密密麻麻的看着有40多种接口不知道为什么人类世界要干出这么多的花样你们这群人类究竟是要干什么啊。
<img src="https://static001.geekbang.org/resource/image/e9/85/e907bb9069217abbbe85f1819c1cbc85.png" alt="" />
我们可以看到,无论是传统世界,还是编程世界,我们都在干一件事情,什么事呢?**那就是通过使用一种更为通用的方式,用另外的话说就是抽象和隔离,让复杂的“世界”变得简单一些**。
然而要做到抽象对于C语言这样的类型语言来说首先要拿出来讲的就是抽象类型这就是所谓的泛型编程。
另外我们还要注意到在编程世界里对于C语言来说类型还可以转换。编译器会使用一切方式来做类型转换因为类型转换有时候可以让我们编程更方便一些也让相近的类型可以做到一点点的泛型。
然而对于C语言的类型转换是会出很多问题的。比如说传给我一个数组这个数组本来是double型的或者是long型 64位的但是如果把数组类型强转成int那么就会出现很多问题因为这会导致程序遍历数组的步长不一样了。
比如:一个 `double a[10]` 的数组,`a[2]` 意味着 `a + sizeof(double) * 2`。如果你把 `a` 强转成 `int`,那么 `a[2]` 就意味着 `a + sizeof(int) * 2`。我们知道 `sizeof(double)``8`,而 `sizeof(int)``4`。于是访问到了不同的地址和内存空间,这就导致程序出现严重的问题。
## C语言的泛型
### 一个泛型的示例 - swap函数
好了我们再看下C语言是如何实现泛型的。C语言的类型泛型基本上来说就是使用`void *`关键字或是使用宏定义。
下面是一个使用了`void*`泛型版本的swap函数。
```
void swap(void* x, void* y, size_t size)
{
char tmp[size];
memcpy(tmp, y, size);
memcpy(y, x, size);
memcpy(x, tmp, size);
}
```
上面这个函数几乎完全改变了int版的函数的实现方式这个实现方式有三个重点
<li>
**函数接口中增加了一个`size`参数**。为什么要这么干呢?因为,用了 `void*` 后,类型被“抽象”掉了,编译器不能通过类型得到类型的尺寸了,所以,需要我们手动地加上一个类型长度的标识。
</li>
<li>
**函数的实现中使用了`memcpy()`函数**。为什么要这样干呢?还是因为类型被“抽象”掉了,所以不能用赋值表达式了,很有可能传进来的参数类型还是一个结构体,因此,为了要交换这些复杂类型的值,我们只能使用内存复制的方法了。
</li>
<li>
**函数的实现中使用了一个`temp[size]`数组**。这就是交换数据时需要用的buffer用buffer来做临时的空间存储。
</li>
于是,新增的`size`参数,使用的`memcpy`内存拷贝以及一个buffer这增加了编程的复杂度。这就是C语言的类型抽象所带来的复杂度的提升。
在提升复杂度的同时,我们发现还有问题,比如,我们想交换两个字符串数组,类型是`char*`,那么,我的`swap()`函数的`x``y`参数是不是要用`void**`了?这样一来,接口就没法定义了。
除了使用 `void*` 来做泛型在C语言中还可以用宏定义来做泛型如下所示
```
#define swap(x, y, size) {\
char temp[size]; \
memcpy(temp, &amp;y, size); \
memcpy(&amp;y, &amp;x, size); \
memcpy(&amp;x, temp, size); \
}
```
但用宏带来的问题就是编译器做字符串替换因为宏是做字符串替换所以会导致代码膨胀导致编译出的执行文件比较大。不过对于swap这个简单的函数来说`void*`和宏替换来说都可以达到泛型。
但是如果我们不是swap而是min()或max()函数,那么宏替换的问题就会暴露得更多一些。比如,对于下面的这个宏:
```
#define min(x, y) (x)&gt;(y) ? (y) : (x)
```
其中一个最大的问题,就是有可能会有**重复执行**的问题。如:
<li>
`min(i++, j++)` 对于这个案例来说,我们本意是比较完后,对变量做累加,但是,因为宏替换的缘故,这会导致变量`i``j`被累加两次。
</li>
<li>
`min(foo(), bar())` 对于这个示例来说,我们本意是比较 `foo()``bar()` 函数的返回值,然而,经过宏替换后,`foo()``bar()` 会被调用两次,这会带来很多问题。
</li>
另外,你会不会觉得无论是用哪种方式,这种“泛型”是不是太宽松了一些,完全不做类型检查,就是在内存上对拷,直接操作内存的这种方式,感觉是不是比较危险,而且就像一个定时炸弹一样,不知道什么时候,在什么条件下就爆炸了。
从上面的两个例子,我们可以发现,无论哪种方式,接口都变得复杂了——加入了`size`,因为如果不加入`size`的话,那么我们的函数内部就需要自己检查`size`。然而,`void*` 这种地址的方式是没法得到`size`的。
而宏定义的那种方式,虽然不会把类型给隐藏掉,可以使用像 `sizeof(x)` 这样的方式得到 `size`。但是如果类型是 `char*`,那么,使用`sizeof`方式只能提到指针类型的`size`,而不是值的`size`。另外,对于不同的类型,比如说`double``int`,那应该用谁的`size`呢?是不是先转一下型呢?这些都是问题。
于是,这种泛型,让我们根本没有办法检查传入参数的`size`,导致我们只能增加接口复杂度,加入一个`size`参数,然后把这个问题抛给调用者了。
### 一个更为复杂的泛型示例 - Search函数
如果我们把这个事情变得更复杂,写个`search`函数,再传一个`int`数组,然后想搜索`target`,搜到返回数组下标,搜不到返回`-1`
```
int search(int* a, size_t size, int target) {
for(int i=0; i&lt;size; i++) {
if (a[i] == target) {
return i;
}
}
return -1;
}
```
我们可以看到,这个函数是类型 `int` 版的。如果我们要把这个函数变成泛型的应该怎么变呢?
就像上面`swap()`函数那样,如果要把它变成泛型,我们需要变更并复杂化函数接口。
<li>
我们需要在函数接口上增加一个element size也就是数组里面每个元素的size。这样当我们遍历数组的时候可以通过这个size正确地移动指针到下一个数组元素。
</li>
<li>
我还要加个`cmpFn`。因为我要去比较数组里的每个元素和`target`是否相等。因为不同数据类型的比较的实现不一样,比如,整型比较用 `==` 就好了。但是如果是一个字符串数组,那么比较就需要用 `strcmp` 这类的函数。而如果你传一个结构体数组如Account账号那么比较两个数据对象是否一样就比较复杂了所以必须要自定义一个比较函数。
</li>
最终我们的`search`函数的泛型版如下所示:
```
int search(void* a, size_t size, void* target,
size_t elem_size, int(*cmpFn)(void*, void*) )
{
for(int i=0; i&lt;size; i++) {
// why not use memcmp()
// use unsigned char * to calculate the address
if ( cmpFn ((unsigned char *)a + elem_size * i, target) == 0 ) {
return i;
}
}
return -1;
}
```
在上面的代码中,我们没有使用`memcmp()`函数,这是因为,如果这个数组是一个指针数组,或是这个数组是一个结构体数组,而结构体数组中有指针成员。我们想比较的是指针指向的内容,而不是指针这个变量。所以,用`memcmp()`会导致我们在比较指针(内存地址),而不是指针所指向的值。
而调用者需要提供如下的比较函数:
```
int int_cmp(int* x, int* y)
{
return *x - *y;
}
int string_cmp(char* x, char* y){
return strcmp(x, y);
}
```
如果面对有业务类型的结构体,可能会是这样的比较函数:
```
typedef struct _account {
char name[10];
char id[20];
} Account;
int account_cmp(Account* x, Account* y) {
int n = strcmp(x-&gt;name, y-&gt;name);
if (n != 0) return n;
return strcmp(x-&gt;id, y-&gt;id);
}
```
我们的C语言干成这个样子看上去还行但是上面的这个`search`函数只能用于数组这样的顺序型的数据容器(数据结构)。如果这个`search`函数能支持一些非顺序型的数据容器数据结构比如堆、栈、哈希表、树、图。那么用C语言来干基本上干不下去了对于像`search()`这样的算法来说,数据类型的自适应问题就已经把事情搞得很复杂了。然而,数据结构的自适应就会把这个事的复杂度搞上几个数量级。
# 小结
这里,如果说,`程序 = 算法 + 数据`我觉得C语言会有这几个问题
<li>
一个通用的算法需要对所处理的数据的数据类型进行适配。但在适配数据类型的过程中C语言只能使用 `void*``宏替换`的方式,这两种方式导致了类型过于宽松,并带来很多其它问题。
</li>
<li>
适配数据类型需要C语言在泛型中加入一个类型的size这是因为我们识别不了被泛型后的数据类型而C语言没有运行时的类型识别所以只能将这个工作抛给调用泛型算法的程序员来做了。
</li>
<li>
算法其实是在操作数据结构,而数据则是放到数据结构中的,所以,真正的泛型除了适配数据类型外,还要适配数据结构,最后这个事情导致泛型算法的复杂急剧上升。比如容器内存的分配和释放,不同的数据体可能有非常不一样的内存分配和释放模型;再比如对象之间的复制,要把它存进来我需要有一个复制,这其中又涉及到是深拷贝,还是浅拷贝。
</li>
<li>
最后,在实现泛型算法的时候,你会发现自己在纠结哪些东西应该抛给调用者处理,哪些又是可以封装起来。如何平衡和选择,并没有定论,也不好解决。
</li>
总体来说C语言设计目标是提供一种能以简易的方式编译、处理低层内存、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。C语言也很适合搭配汇编语言来使用。C语言把非常底层的控制权交给了程序员它设计的理念是
- 相信程序员;
- 不会阻止程序员做任何底层的事;
- 保持语言的最小和最简的特性;
- 保证C语言的最快的运行速度那怕牺牲移值性。
从某种角度上来说C语言的伟大之处在于——**使用C语言的程序员在高级语言的特性之上还能简单地做任何底层上的微观控制**。这是C语言的强大和优雅之处。也有人说C语言是高级语言中的汇编语言。
不过这只是在针对底层指令控制和过程式的编程方式。而对于更高阶、更为抽象的编程模型来说C语言这种基于过程和底层的初衷设计方式就会成为它的短板。因为在编程这个世界中更多的编程工作是解决业务上的问题而不是计算机的问题所以我们需要更为贴近业务、更为抽象的语言。
说到这里我想你会问那C语言本会怎么去解决这些问题呢简单点说C语言并没有解决这些问题所以才有了后面的C++等其他语言下一篇文章中我也会和你聊聊C++是如何解决这些问题的。
C语言诞生于1972年到现在已经有45年的历史在它之后C++、Java、C#等语言前仆后继,一浪高过一浪,都在试图解决那个时代的那个特定问题。我们不能去否定某个语言,但可以确定的是,随着历史的发展,每一门语言都还在默默迭代,不断优化和更新。同时,也会有很多新的编程语言带着新的闪光耀眼的特性出现在我们面前。
再回过头来说,编程范式其实就是程序的指导思想,它也代表了这门语言的设计方向,我们并不能说哪种范式更为超前,只能说各有千秋。
比如C语言就是过程式的编程语言像C语言这样的过程式编程语言优点是底层灵活而且高效特别适合开发运行较快且对系统资源利用率要求较高的程序但我上面抛出的问题它在后来也没有试图去解决因为编程范式的选择基本已经决定了它的“命运”。
我们怎么解决上述C语言没有解决好的问题呢请期待接下来的文章。

View File

@@ -0,0 +1,460 @@
上一篇文章中我从C语言开始说起聊了聊面向过程式的编程范式相信从代码的角度你对这类型的语言已经有了一些理解。作为一门高级语言C语言绝对是编程语言历史发展中的一个重要里程碑但随着认知的升级面向过程的C语言已经无法满足更高层次的编程的需要。于是C++出现了。
# C++语言
1980年AT&amp;T贝尔实验室的**Bjarne Stroustrup**创建的C++语言横空出世它既可以全面兼容C语言又巧妙揉和了一些面向对象的编程理念。现在来看不得不佩服Stroustrup的魄力。在这里我也向你推荐一本书书名是《C++语言的设计和演化》。
这本书系统介绍了C++诞生的背景以及初衷,书的作者就是[Stroustrup](https://book.douban.com/author/362072/)本人所以你可以非常详细地从语言创建者的角度了解他的设计思路和创新之旅。当然就是在今天C++这门语言也还有很多争议,这里我不细说。如果你感兴趣的话,可以看看我几年前在酷壳上发表的文章《[C++的坑真的多吗?](https://coolshell.cn/articles/7992.html)》。
从语言角度来说实际上早期C++的许多工作是对C的强化和净化并把完全兼容C作为强制性要求这也是C++复杂晦涩的原因这点Java就干得比C++彻底得多。在C89、C99这两个C语言的标准中有许多改进都是从C++中引进的。
可见C++对C语言的贡献非常之大。是的因为C++很大程度就是用来解决C语言中的各种问题和各种不方便的。比如
<li>
用引用来解决指针的问题。
</li>
<li>
用namespace来解决名字空间冲突的问题。
</li>
<li>
通过try-catch来解决检查返回值编程的问题。
</li>
<li>
用class来解决对象的创建、复制、销毁的问题从而可以达到在结构体嵌套时可以深度复制的内存安全问题。
</li>
<li>
通过重载操作符来达到操作上的泛型。(比如,消除[《01 | 编程范式游记:起源》](https://time.geekbang.org/column/article/301)中提到的比较函数`cmpFn`,再比如用`&gt;&gt;`操作符消除`printf()`的数据类型不够泛型的问题。)
</li>
<li>
通过模板template和虚函数的多态以及运行时识别来达到更高层次的泛型和多态。
</li>
<li>
用RAII、智能指针的方式解决了C语言中因为需要释放资源而出现的那些非常ugly也很容易出错的代码的问题。
</li>
<li>
用STL解决了C语言中算法和数据结构的N多种坑。
</li>
# C++泛型编程
C++是支持编程范式最多的一门语言它虽然解决了很多C语言的问题但我个人觉得它最大的意义是解决了C语言泛型编程的问题。因为我们可以看到一些C++的标准规格说明书里有一半以上都在说明STL的标准规格应该是什么样的这说明泛型编程是C++重点中的重点。
理想情况下,算法应是和数据结构以及类型无关的,各种特殊的数据类型理应做好自己分内的工作,算法只关心一个标准的实现。**而对于泛型的抽象,我们需要回答的问题是,如果我们的数据类型符合通用算法,那么对数据类型的最小需求又是什么呢?**
我们来看看C++是如何有效解决程序泛型问题的,我认为有三点。
**第一,它通过类的方式来解决**
- 类里面会有构造函数、析构函数表示这个类的分配和释放。
- 还有它的拷贝构造函数,表示了对内存的复制。
- 还有重载操作符,像我们要去比较大于、等于、不等于。
这样可以让一个用户自定义的数据类型和内建的那些数据类型就很一致了。
**第二,通过模板达到类型和算法的妥协**
- 模板有点像DSL模板的特化会根据使用者的类型在编译时期生成那个模板的代码。
- 模板可以通过一个虚拟类型来做类型绑定,这样不会导致类型转换时的问题。
模板很好地取代了C时代宏定义带来的问题。
**第三,通过虚函数和运行时类型识别**
- 虚函数带来的多态在语义上可以支持“同一类”的类型泛型。
- 运行时类型识别技术可以做到在泛型时对具体类型的特殊处理。
这样一来,就可以写出基于抽象接口的泛型。
拥有了这些C++引入的技术我们就可以做到C语言很难做到的泛型编程了。
正如前面说过的,一个良好的泛型编程需要解决如下几个泛型编程的问题:
1. 算法的泛型;
1. 类型的泛型;
1. 数据结构(数据容器)的泛型。
## C++泛型编程的示例 - Search函数
就像前面的`search()`函数,里面的 `for(int i=0; i&lt;len; i++)` 这样的遍历方式,只能适用于**顺序型的数据结构**的方式迭代array、set、queue、list和link等。并不适用于**非顺序型的数据结构**。
如哈希表hash table二叉树binary tree、图graph等这样数据不是按顺序存放的数据结构数据容器。所以如果找不到一种**泛型的数据结构的操作方式(如遍历、查找、增加、删除、修改……)**,那么,任何的算法或是程序都不可能做到真正意义上的泛型。
除了`search()`函数的“遍历操作”之外还有search函数的返回值是一个整型的索引下标。这个整型的下标对于“顺序型的数据结构”是没有问题的但是对于“非顺序的数据结构”在语义上都存在问题。
比如如果我要在一个hash table中查找一个key返回什么呢一定不是返回“索引下标”因为在hash table这样的数据结构中数据的存放位置不是顺序的而且还会因为容量不够的问题被重新hash后改变所以返回数组下标是没有意义的。
对此,我们要把这个事做得泛型和通用一些。如果找到,返回找到的这个元素的一个指针(地址)会更靠谱一些。
所以为了解决泛型的问题我们需要动用以下几个C++的技术。
<li>
使用模板技术来抽象类型,这样可以写出类型无关的数据结构(数据容器)。
</li>
<li>
使用一个迭代器来遍历或是操作数据结构内的元素。
</li>
我们来看一下C++版的`search()`函数是什么样的。
先重温一下C语言版的代码
```
int search(void* a, size_t size, void* target,
size_t elem_size, int(*cmpFn)(void*, void*) )
{
for(int i=0; i&lt;size; i++) {
if ( cmpFn (a + elem_size * i, target) == 0 ) {
return i;
}
}
return -1;
}
```
我们再来看一下C++泛型版的代码:
```
template&lt;typename T, typename Iter&gt;
Iter search(Iter pStart, Iter pEnd, T target)
{
for(Iter p = pStart; p != pEnd; p++) {
if ( *p == target )
return p;
}
return NULL;
}
```
在C++的泛型版本中,我们可以看到:
<li>
使用`typename T`抽象了数据结构中存储数据的类型。
</li>
<li>
使用`typename Iter`,这是不同的数据结构需要自己实现的“迭代器”,这样也就抽象掉了不同类型的数据结构。
</li>
<li>
然后,我们对数据容器的遍历使用了`Iter`中的`++`方法,这是数据容器需要重载的操作符,这样通过操作符重载也就泛型掉了遍历。
</li>
<li>
在函数的入参上使用了`pStart``pEnd`来表示遍历的起止。
</li>
<li>
使用`*Iter`来取得这个“指针”的内容。这也是通过重载 `*` 取值操作符来达到的泛型。
</li>
当然,你可能会问,为什么我们不用标准接口`Iter.Next()`取代`++``Iter.GetValue()`来取代`*`而是通过重载操作符其实这样做是为了兼容原有C语言的编程习惯。
说明一下,所谓的`Iter`,在实际代码中,就是像`vector&lt;int&gt;::iterator``map&lt;int, string&gt;::iterator`这样的东西。这是由相应的数据容器来实现和提供的。
下面是C++ STL中的`find()`函数的代码。
```
template&lt;class InputIterator, class T&gt;
InputIterator find (InputIterator first, InputIterator last, const T&amp; val)
{
while (first!=last) {
if (*first==val) return first;
++first;
}
return last;
}
```
## C++泛型编程示例 - Sum 函数
也许你觉得到这一步,我们的泛型设计就完成了。其实,还远远不够。`search`函数只是一个开始,我们还有很多别的算法会让问题变得更为复杂。
我们再来看一个`sum()`函数。
先看C语言版
```
long sum(int *a, size_t size) {
long result = 0;
for(int i=0; i&lt;size; i++) {
result += a[i];
}
return result;
}
```
再看一下C++泛型的版本:
```
template&lt;typename T, typename Iter&gt;
T sum(Iter pStart, Iter pEnd) {
T result = 0;
for(Iter p=pStart; p!=pEnd; p++) {
result += *p;
}
return result;
}
```
你看到了什么样的问题?这个代码中最大的问题就是 `T result = 0;` 这条语句:
- 那个`0`假设了类型是`int`
- 那个`T`假设了Iter中出来的类型是`T`
这样的假设是有问题的如果类型不一样就会导致转型的问题这会带来非常buggy的代码。那么我们怎么解决呢
## C++泛型编程的重要技术 - 迭代器
我们知道`Iter`在实际调用者那会是一个具体的像`vector&lt;int&gt;::iterator`这样的东西。在这个声明中,`int`已经被传入`Iter`中了。所以,定义`result``T`应该可以从`Iter`中来。这样就可以保证类型是一样的,而且不会有被转型的问题。
所以我们需要精心地实现一个“迭代器”。下面是一个“精简版”的迭代器我没有把C++ STL代码里的迭代器列出来是因为代码太多太复杂我这里只是为了说明问题
```
template &lt;class T&gt;
class container {
public:
class iterator {
public:
typedef iterator self_type;
typedef T value_type;
typedef T* pointer;
typedef T&amp; reference;
reference operator*();
pointer operator-&gt;();
bool operator==(const self_type&amp; rhs)
bool operator!=(const self_type&amp; rhs)
self_type operator++() { self_type i = *this; ptr_++; return i; }
self_type operator++(int junk) { ptr_++; return *this; }
...
...
private:
pointer _ptr;
};
iterator begin();
iterator end();
...
...
};
```
上面的代码是我写的一个迭代器(这个迭代器在语义上是没有问题的),我没有把所有的代码列出来,而把它的一些基本思路列了出来。这里我说明一下几个关键点。
<li>
首先,一个迭代器需要和一个容器在一起,因为里面是对这个容器的具体的代码实现。
</li>
<li>
它需要重载一些操作符,比如:取值操作`*`、成员操作`-&gt;`、比较操作`==``!=`,还有遍历操作`++`,等等。
</li>
<li>
然后,还要`typedef`一些类型,比如`value_type`,告诉我们容器内的数据的实际类型是什么样子。
</li>
<li>
还有一些,如`begin()``end()`的基本操作。
</li>
<li>
我们还可以看到其中有一个`pointer _ptr`的内部指针来指向当前的数据(注意,`pointer`就是 `T*`)。
</li>
好了,有了这个迭代器后,我们还要解决`T result = 0`后面的这个`0`的问题。这个事,算法没有办法搞定,最好由用户传入。于是出现了下面最终泛型的`sum()`版函数。
```
template &lt;class Iter&gt;
typename Iter::value_type
sum(Iter start, Iter end, T init) {
typename Iter::value_type result = init;
while (start != end) {
result = result + *start;
start++;
}
return result;
}
```
我们可以看到`typename Iter::value_type result = init`这条语句是关键。我们解决了所有的问题。
我们如下使用:
```
container&lt;int&gt; c;
container&lt;int&gt;::iterator it = c.begin();
sum(c.begin(), c.end(), 0);
```
这就是整个STL的泛型方法其中包括
- 泛型的数据容器;
- 泛型数据容器的迭代器;
- 然后泛型的算法就很容易写了。
# 需要更多的抽象
## 更为复杂的需求
但是还能不能做到更为泛型呢比如如果我们有这样的一个数据结构Employee里面有vacation就是休假多少天以及工资。
```
struct Employee {
string name;
string id;
int vacation;
double salary
};
```
现在我想计算员工的总薪水,或是总休假天数。
```
vector&lt;Employee&gt; staff;
//total salary or total vacation days?
sum(staff.begin(), staff.end(), 0);
```
我们的`sum`完全不知道怎么搞了,因为要累加的是`Employee`类中的不同字段即便我们的Employee中重载了`+`操作,也不知道要加哪个字段。
另外我们可能还会有求平均值average求最小值min求最大值max求中位数mean等等。你会发现算法写出来基本上都是一样的只是其中的“累加”操作变成了另外一个操作。就这个例子而言我想计算员工薪水里面最高的和休假最少的或者我想计算全部员工的总共休假多少天。那么面对这么多的需求我们是否可以泛型一些呢怎样解决这些问题呢
## 更高维度的抽象
要解决这个问题,我希望我的这个算法只管遍历,具体要干什么,那是业务逻辑,由外面的调用方来定义我就好了,和我无关。这样一来,代码的重用度就更高了。
下面是一个抽象度更高的版本,这个版本再叫`sum`就不太合适了。这个版本应该是`reduce`——用于把一个数组reduce成一个值。
```
template&lt;class Iter, class T, class Op&gt;
T reduce (Iter start, Iter end, T init, Op op) {
T result = init;
while ( start != end ) {
result = op( result, *start );
start++;
}
return result;
}
```
上面的代码中我们需要传一个函数进来。在STL中它是个函数对象我们还是这套算法但是result不是像前面那样去加是把整个迭代器值给你一个operation然后由它来做。我把这个方法又拿出去了所以就会变成这个样子。
在C++ STL中与我的这个reduce函数对应的函数名叫 `accumulate()`,其实际代码有两个版本。
第一个版本就是上面的版本,只不过是用`for`语句而不是`while`
```
template&lt;class InputIt, class T&gt;
T accumulate(InputIt first, InputIt last, T init)
{
for (; first != last; ++first) {
init = init + *first;
}
return init;
}
```
第二个版本,更为抽象,因为需要传入一个“二元操作函数”——`BinaryOperation op`来做accumulate。accumulate的语义比sum更抽象了。
```
template&lt;class InputIt, class T, class BinaryOperation&gt;
T accumulate(InputIt first, InputIt last, T init,
BinaryOperation op)
{
for (; first != last; ++first) {
init = op(init, *first);
}
return init;
}
```
来看看我们在使用中是什么样子的:
```
double sum_salaries =
reduce( staff.begin(), staff.end(), 0.0,
[](double s, Employee e)
{return s + e.salary;} );
double max_salary =
reduce( staff.begin(), staff.end(), 0.0,
[](double s, Employee e)
{return s &gt; e.salary? s: e.salary; } );
```
注意我这里用了C++的lambda表达式。
你可以很清楚地看到reduce这个函数就更通用了具体要干什么样的事情呢放在匿名函数里面它会定义我我只做一个reduce。更抽象地来说我就把一个数组一个集合变成一个值。怎么变成一个值呢由这个函数来决定。
### Reduce 函数
我们来看看如何使用reduce和其它函数完成一个更为复杂的功能。
下面这个示例中,我先定义了一个函数对象`counter`。这个函数对象需要一个`Cond`的函数对象它是个条件判断函数如果满足条件则加1否则加0。
```
template&lt;class T, class Cond&gt;
struct counter {
size_t operator()(size_t c, T t) const {
return c + (Cond(t) ? 1 : 0);
}
};
```
然后,我用上面的`counter`函数对象和`reduce`函数共同来打造一个`counter_if`算法(当条件满足的时候我就记个数,也就是统计满足某个条件的个数),我们可以看到,就是一行代码的事。
```
template&lt;class Iter, class Cond&gt;
size_t count_if(Iter begin, Iter end, Cond c){
return reduce(begin, end, 0,
counter&lt;Iter::value_type, Cond&gt;(c));
}
```
至于是什么样的条件,这个属于业务逻辑,不是我的流程控制,所以,这应该交给使用方。
于是当我需要统计薪资超过1万元的员工的数量时一行代码就完成了。
```
size_t cnt = count_if(staff.begin(), staff.end(),
[](Employee e){ return e.salary &gt; 10000; });
```
Reduce时可以只对结构体中的某些值做Reduce比如说只对 `salary&gt;10000` 的人做只选出这个里面的值它用Reduce就可以达到这步只要传不同的方式给它你就可以又造出一个新的东西出来。
说着说着就到了函数式编程。函数式编程里面我们可以用很多的像reduce这样的函数来完成更多的像STL里面的`count_if()`这样有具体意义的函数。关于函数式编程,我们会在后面继续具体聊。
# 小结
在这篇文章中我们聊到C++语言是如何通过泛型来解决C语言遇到的问题其实这里面主要就是泛型编程和函数式编程的基本方法相关的细节虽然解决编程语言中类型带来的问题可能有多种方式不一定就是C++这种方式。
而我之所以从C/C++开始目的只是因为C/C++都是比较偏底层的编程语言。从底层的原理上我们可以更透彻地了解从C到C++的演进这一过程中带来的编程方式的变化。这可以让你看到,在静态类型语言方面解决泛型编程的一些技术和方法,从而感受到其中的奥妙和原理。
**因为形式是多样的,但是原理是相通的,所以,这个过程会非常有助于你更深刻地了解后面会谈到的更多的编程范式**
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)

View File

@@ -0,0 +1,230 @@
前面我们讨论了从C到C++的泛型编程方法并且初探了更为抽象的函数式编程。正如在上一篇文章中所说的泛型编程的方式并不只有C++这一种类型,我们只是通过这个过程了解一下,底层静态类型语言的泛型编程原理。这样能够方便我们继续后面的历程。
是的除了C++那样的泛型,如果你了解其它编程语言一定会发现,在动态类型语言或是某些有语法糖支持的语言中,那个`swap()``search()` 函数的泛型其实可以很简单地就实现了。
比如,你甚至可以把`swap()`函数简单地写成下面这个样子包括Go语言也有这样的语法
```
b, a = a, b;
```
在上一篇文章后面的Reduce函数中可以看到在编程世界中我们需要处理好两件事
- 第一件事是编程语言中的类型问题。
- 第二件事是对真实世界中业务代码的抽象、重用和拼装。
所以,在这篇文章中,我们还是继续深入地讨论上面这两个问题,着重讨论一下编程语言中的类型系统和泛型编程的本质。
# 类型系统
在计算机科学中,类型系统用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,以及如何操作这些类型,还有这些类型如何互相作用。类型可以确认一个值或者一组值,具有特定的意义和目的。
一般来说编程语言会有两种类型一种是内建类型如int、float和char等一种是抽象类型如struct、class和function等。抽象类型在程序运行中可能不表示为值。类型系统在各种语言之间有非常大的不同也许最主要的差异存在于编译时期的语法以及运行时期的操作实现方式。
编译器可能使用值的静态类型以最优化所需的存储区并选取对数值运算时的最佳算法。例如在许多C编译器中“浮点数”数据类型是以32比特表示与IEEE 754规格一致的单精度浮点数。因此在数值运算上C应用了浮点数规范浮点数加法、乘法等
类型的约束程度以及评估方法,影响了语言的类型。更进一步讲,编程语言可能就类型多态性部分,对每一个类型都对应了一个针对于这个类型的算法运算。类型理论研究类型系统,尽管实际的编程语言类型系统,起源于计算机架构的实际问题、编译器实现,以及语言设计。
程序语言的类型系统主要提供如下的功能。
<li>
**程序语言的安全性**。使用类型可以让编译器侦测一些代码的错误,例如:可以识别出一个错误无效的表达式,如`“Hello, World” + 3`这样的不同数据类型间操作的问题。强类型语言提供更多的安全性,但是并不能保证绝对的安全。
</li>
<li>
**利于编译器的优化**。 静态类型语言的类型声明,可以让编译器明确地知道程序员的意图。因此,编译器就可以利用这一信息做很多代码优化工作。例如:如果我们指定一个类型是 `int` 那么编译就知道这个类型会以4个字节的倍数进行对齐编译器就可以非常有效地利用更有效率的机器指令。
</li>
<li>
**代码的可读性**。有类型的编程语言,可以让代码更易读和更易维护,代码的语义也更清楚,代码模块的接口(如函数)也更丰富和清楚。
</li>
<li>
**抽象化**。类型允许程序设计者对程序以较高层次的方式思考,而不是烦人的低层次实现。例如,我们使用整型或是浮点型来取代底层的字节实现,我们可以将字符串设计成一个值,而不是底层字节的数组。从高层上来说,类型可以用来定义不同模块间的交互协议,比如函数的入参类型和返回类型,从而可以让接口更有语义,而且不同的模块数据交换更为直观和易懂。
</li>
但是,正如前面说的,**类型带来的问题就是我们作用于不同类型的代码,虽然长得非常相似,但是由于类型的问题需要根据不同版本写出不同的算法,如果要做到泛型,就需要涉及比较底层的玩法**。
对此这个世界出现了两类语言一类是静态类型语言如C、C++、Java一种是动态类型语言如Python、PHP、JavaScript等。
我们来看一下,一段动态类型语言的代码:
```
x = 5;
x = &quot;hello&quot;;
```
在这个示例中,我们可以看到变量 `x` 一开始好像是整型,然后又成了字符串型。如果在静态类型的语言中写出这样的代码,那么就会在编译期出错。而在动态类型的语言中,会以类型标记维持程序所有数值的“标记”,并在运算任何数值之前检查标记。所以,一个变量的类型是由运行时的解释器来动态标记的,这样就可以动态地和底层的计算机指令或内存布局对应起来。
我们再来看一个示例对于JavaScript这样的动态语言来说可以定义出下面这样的数据结构一个数组的元素可以是各式各样的类型这在静态类型的语言中是很难做到的。
```
var a = new Array()
a[0] = 2017;
a[1] = &quot;Hello&quot;;
a[2] = {name: &quot;Hao Chen&quot;};
```
>
注:其实,这并不是一个数组,而是一个 `key:value`。因为动态语言的类型是动态的所以key 和 value 的类型都可以随意。比如,对于 `a` 这个数据结构,还可以写成:`a[&quot;key&quot;] = &quot;value&quot;` 这样的方式。
在弱类型或是动态类型的语言中,下面代码的执行会有不确定的结果。
```
x = 5;
y = &quot;37&quot;;
z = x + y;
```
<li>
有的像Visual Basic语言给出的结果是42系统将字符串&quot;37&quot;转换成数字37以匹配运算上的直觉。
</li>
<li>
而有的像JavaScript语言给出的结果是&quot;537&quot;系统将数字5转换成字符串&quot;5&quot;并把两者串接起来。
</li>
<li>
像Python这样的语言则会产生一个运行时错误。
</li>
但是,**我们需要清楚地知道,无论哪种程序语言,都避免不了一个特定的类型系统**。哪怕是可随意改变变量类型的动态类型的语言,我们在读代码的过程中也需要脑补某个变量在运行时的类型。
所以,每个语言都需要一个类型检查系统。
<li>
静态类型检查是在编译器进行语义分析时进行的。如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换),那么称此处理为强类型,反之称为弱类型。
</li>
<li>
动态类型检查系统更多的是在运行时期做动态类型标记和相关检查。所以,动态类型的语言必然要给出一堆诸如:`is_array()`, `is_int()`, `is_string()` 或是 `typeof()` 这样的运行时类型检查函数。
</li>
总之,“类型”有时候是一个有用的事,有时候又是一件很讨厌的事情。因为类型是对底层内存布局的一个抽象,会让我们的代码要关注于这些非业务逻辑上的东西。而且,我们的代码需要在不同类型的数据间做处理。但是如果程序语言类型检查得过于严格,那么,我们写出来的代码就不能那么随意。
所以对于静态类型的语言也开了些“小后门”比如类型转换还有C++、Java运行时期的类型测试。
这些小后门也会带来相当讨厌的问题比如下面这个C语言的示例。
```
int x = 5;
char y[] = &quot;37&quot;;
char* z = x + y;
```
在上面这个例子中结果可能和你想的完全不一样。由于C语言的底层特性这个例子中的 `z` 会指向一个超过 `y` 地址 5个字节的内存地址相当于指向y字符串的指针之后的两个空字符处。
静态类型语言的支持者和动态类型自由形式的支持者,经常发生争执。前者主张,在编译的时候就可以较早发现错误,而且还可增进运行时期的性能。
后者主张,使用更加动态的类型系统,分析代码更为简单,减少出错机会,才能更加轻松快速地编写程序。与此相关的是,后者还主张,考虑到在类型推断的编程语言中,通常不需要手动宣告类型,这部分的额外开销也就自动降低了。
在本系列内容的前两篇文章中我们用C/C++语言来做泛型编程的示例,似乎动态类型语言能够比较好地规避类型导致需要出现多个版本代码的问题,这样可以让我们更好地关注于业务。
但是,我们需要清楚地明白,**任何语言都有类型系统**,只是动态类型语言在运行时做类型检查。动态语言的代码复杂度比较低,并可以更容易地关注业务,在某些场景下是对的,但有些情况下却并不见得。
比如在JavaScript中我们需要做一个变量转型的函数可能会是下面这个样子
```
function ToNumber(x) {
switch(typeof x) {
case &quot;number&quot;: return x;
case &quot;undefined&quot;: return NaN;
case &quot;boolean&quot;: return x ? 1 : 0;
case &quot;string&quot;: return Number(x);
case &quot;object&quot;: return NaN;
case &quot;function&quot;: return NaN;
}
}
```
我相信,你在动态类型语言的代码中可以看到大量类似 `typeof` 这样的类型检查代码。是的,这是动态类型带来的另一个问题,就是运行时识别(这个是比较耗性能的)。
如果你用过一段时间的动态类型语言,一旦代码量比较大了,我们就会发现,代码中出现“类型问题”而引发整个程序出错的情况实在是太多太多了。而且,这样的出错会让整个程序崩溃掉,太恐怖了。这个时候,我们就很希望提前发现这些类型的问题。
静态语言的支持者会说编译器能帮我们找到这些问题,而动态语言的支持者则认为,静态语言的编译器也无法找到所有的问题,想真正提前找到问题只能通过测试来解决。其实他们都对。
# 泛型的本质
要了解泛型的本质,就需要了解类型的本质。
<li>
类型是对内存的一种抽象。不同的类型,会有不同的内存布局和内存分配的策略。
</li>
<li>
不同的类型,有不同的操作。所以,对于特定的类型,也有特定的一组操作。
</li>
所以,要做到泛型,我们需要做下面的事情:
<li>
标准化掉类型的内存分配、释放和访问。
</li>
<li>
标准化掉类型的操作。比如比较操作I/O操作复制操作……
</li>
<li>
标准化掉数据容器的操作。比如:查找算法、过滤算法、聚合算法……
</li>
<li>
标准化掉类型上特有的操作。需要有标准化的接口来回调不同类型的具体操作……
</li>
所以C++动用了非常繁多和复杂的技术来达到泛型编程的目标。
<li>
通过类中的构造、析构、拷贝构造,重载赋值操作符,标准化(隐藏)了类型的内存分配、释放和复制的操作。
</li>
<li>
通过重载操作符,可以标准化类型的比较等操作。
</li>
<li>
通过iostream标准化了类型的输入、输出控制。
</li>
<li>
通过模板技术(包括模板的特化),来为不同的类型生成类型专属的代码。
</li>
<li>
通过迭代器来标准化数据容器的遍历操作。
</li>
<li>
通过面向对象的接口依赖(虚函数技术),来标准化了特定类型在特定算法上的操作。
</li>
<li>
通过函数式(函数对象),来标准化对于不同类型的特定操作。
</li>
通过学习C++我们可以看到一个比较完整的泛型编程里所涉及的编程范式这些编程泛式在其它语言中都会或多或少地体现着。比如JDK 5 引入的泛型类型就源自C++的模板。
泛型编程于1985年在论文 [Generic Programming](http://stepanovpapers.com/genprog.pdf) 中被这样定义:
>
Generic programming centers around the idea of abstracting from concrete, efficient algorithms to obtain generic algorithms that can be combined with different data representations to produce a wide variety of useful software.
Musser, David R.; Stepanov, Alexander A., Generic Programming
我理解其本质就是 —— **屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型。**
# 小结
在编程语言中类型系统的出现主要是对容许混乱的操作加上了严格的限制以避免代码以无效的数据使用方式编译或运行。例如整数运算不可用于字符串指针的操作不可用于整数上等等。但是类型的产生和限制虽然对底层代码来说是安全的但是对于更高层次的抽象产生了些负面因素。比如在C++语言里,为了同时满足静态类型和抽象,就导致了模板技术的出现,带来了语言的复杂性。
我们需要清楚地明白编程语言本质上帮助程序员屏蔽底层机器代码的实现而让我们可以更为关注于业务逻辑代码。但是因为编程语言作为机器代码和业务逻辑的粘合层是在让程序员可以控制更多底层的灵活性还是屏蔽底层细节让程序员可以更多地关注于业务逻辑这是很难两全需要trade-off的事。
所以不同的语言在设计上都会做相应的取舍比如C语言偏向于让程序员可以控制更多的底层细节而Java和Python则让程序员更多地关注业务功能的实现。而C++则是两者都想要,导致语言在设计上非常复杂。
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)

View File

@@ -0,0 +1,627 @@
从前三章内容中我们了解到虽然C语言简单灵活能够让程序员在高级语言特性之上轻松进行底层上的微观控制被誉为“高级语言中的汇编语言”但其基于过程和底层的设计初衷又成了它的短板。
在程序世界中编程工作更多的是解决业务上的问题而不是计算机的问题我们需要更为贴近业务、更为抽象的语言如典型的面向对象语言C++和Java等。
C++很大程度上解决了C语言中的各种问题和不便尤其是通过类、模板、虚函数和运行时识别等解决了C语言的泛型编程问题。然而如何做更为抽象的泛型呢答案就是函数式编程Functional Programming
# 函数式编程
相对于计算机的历史而言,函数式编程其实是一个非常古老的概念。函数式编程的基础模型来源于 λ 演算,而 λ 演算并没有被设计在计算机上执行。它是由 Alonzo Church 和 Stephen Cole Kleene 在 20 世纪 30 年代引入的一套用于研究函数定义、函数应用和递归的形式系统。
如 Alonzo 所说,像 booleans、integers 或者其他的数据结构都可以被函数取代掉。
<img src="https://static001.geekbang.org/resource/image/7f/cd/7fac133e887bb91f6619887e6a6dcfcd.png" alt="" />
我们来看一下函数式编程,它的理念就来自于数学中的代数。
```
f(x)=5x^2+4x+3
g(x)=2f(x)+5=10x^2+8x+11
h(x)=f(x)+g(x)=15x^2+12x+14
```
假设f(x)是一个函数g(x)是第二个函数把f(x)这个函数套下来,并展开。然后还可以定义一个由两个一元函数组合成的二元函数,还可以做递归,下面这个函数定义就是斐波那契数列。
```
f(x)=f(x-1)+f(x-2)
```
对于函数式编程来说,它只关心**定义输入数据和输出数据相关的关系数学表达式里面其实是在做一种映射mapping输入的数据和输出的数据关系是什么样的是用函数来定义的**。
函数式编程有以下特点。
**特征**
- **stateless**函数不维护任何状态。函数式编程的核心精神是stateless简而言之就是它不能存在状态打个比方你给我数据我处理完扔出来。里面的数据是不变的。
- **immutable**:输入数据是不能动的,动了输入数据就有危险,所以要返回新的数据集。
**优势**
- 没有状态就没有伤害。
- 并行执行无伤害。
- Copy-Paste重构代码无伤害。
- 函数的执行没有顺序上的问题。
<img src="https://static001.geekbang.org/resource/image/13/a2/134bd812c06ca16d8f29bc40174055a2.png" alt="" />
函数式编程还带来了以下一些好处。
<li>
**惰性求值**。这需要编译器的支持,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。也就是说,语句如 `x:=expression;` (把一个表达式的结果赋值给一个变量)显式地调用这个表达式被计算并把结果放置到 `x` 中,但是先不管实际在 `x` 中的是什么,直到通过后面的表达式中到 `x` 的引用而有了对它的值的需求的时候,而后面表达式自身的求值也可以被延迟,最终为了生成让外界看到的某个符号而计算这个快速增长的依赖树。
</li>
<li>
**确定性**。所谓确定性,就是像在数学中那样,`f(x) = y` 这个函数无论在什么场景下,都会得到同样的结果,而不是像程序中的很多函数那样。同一个参数,在不同的场景下会计算出不同的结果,这个我们称之为函数的确定性。所谓不同的场景,就是我们的函数会根据运行中的状态信息的不同而发生变化。
</li>
我们知道因为状态在并行执行和copy-paste时引发bug的概率是非常高的所以没有状态就没有伤害就像没有依赖就没有伤害一样并行执行无伤害copy代码无伤害因为没有状态代码怎样拷都行。
**劣势**
- 数据复制比较严重。
>
**注**有一些人可能会觉得这会对性能造成影响。其实这个劣势不见得会导致性能不好。因为没有状态所以代码在并行上根本不需要锁不需要对状态修改的锁所以可以拼命地并发反而可以让性能很不错。比如Erlang就是其中的代表。
对于纯函数式(也就是完全没有状态的函数)的编程来说,各个语言支持的程度如下:
- 完全纯函数式 Haskell
- 容易写纯函数 F#, Ocaml, Clojure, Scala
- 纯函数需要花点精力 C#, Java, JavaScript
完全纯函数的语言,很容易写成函数,纯函数需要花精力。只要所谓的纯函数的问题,传进来的数据不改,改完的东西复制一份拷出去,然后没有状态显示。
但是很多人并不习惯函数式编程,因为函数式编程和过程式编程的思维方式完全不一样。过程式编程是在把具体的流程描述出来,所以可以不假思索,而函数式编程的抽象度更大,在实现方式上,有函数套函数、函数返回函数、函数里定义函数……把人搞得很糊涂。
# 函数式编程用到的技术
下面是函数式编程用到的一些技术。
<li>
**first class function头等函数** :这个技术可以让你的函数就像变量一样来使用。也就是说,你的函数可以像变量一样被创建、修改,并当成变量一样传递、返回,或是在函数中嵌套函数。
</li>
<li>
**tail recursion optimization尾递归优化** 我们知道递归的害处那就是如果递归很深的话stack受不了并会导致性能大幅度下降。因此我们使用尾递归优化技术——每次递归时都会重用stack这样能够提升性能。当然这需要语言或编译器的支持。Python就不支持。
</li>
<li>
**map &amp; reduce** 这个技术不用多说了函数式编程最常见的技术就是对一个集合做Map和Reduce操作。这比起过程式的语言来说在代码上要更容易阅读。传统过程式的语言需要使用for/while循环然后在各种变量中把数据倒过来倒过去的这个很像C++ STL中foreach、find_if、count_if等函数的玩法。
</li>
<li>
**pipeline管道**这个技术的意思是将函数实例成一个一个的action然后将一组action放到一个数组或是列表中再把数据传给这个action list数据就像一个pipeline一样顺序地被各个函数所操作最终得到我们想要的结果。
</li>
<li>
**recursing递归** :递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。
</li>
<li>
**currying柯里化** :将一个函数的多个参数分解成多个函数, 然后将函数多层封装起来每层函数都返回一个函数去接收下一个参数这可以简化函数的多个参数。在C++中这很像STL中的bind1st或是bind2nd。
</li>
<li>
**higher order function高阶函数**:所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出,就像面向对象对象满天飞一样。这个技术用来做 Decorator 很不错。
</li>
上面这些技术太抽象了,我们还是从一个最简单的例子开始。
```
// 非函数式不是pure funciton有状态
int cnt;
void increment(){
cnt++;
}
```
这里有个全局变量,调这个全局函数变量++,这里面是有状态的,这个状态在外部。所以,如果是多线程的话,这里面的代码是不安全的。
如果写成纯函数,应该是下面这个样子。
```
// 函数式pure function 无状态
int increment(int cnt){
return cnt+1;
}
```
这个是你传给我什么,我就返回这个值的+1值你会发现代码随便拷而且与线程无关代码在并行时候不用锁因为是复制了原有的数据并返回了新的数据。
我们再来看另一个例子:
```
def inc(x):
def incx(y):
return x+y
return incx
inc2 = inc(2)
inc5 = inc(5)
print inc2(5) # 输出 7
print inc5(5) # 输出 10
```
上面这段Python的代码开始有点复杂了。我们可以看到上面那个例子`inc()`函数返回了另一个函数`incx()`,于是可以用`inc()`函数来构造各种版本的inc函数比如`inc2()``inc5()`。这个技术其实就是上面所说的 currying 技术.从这个技术上,你可能体会到函数式编程的理念。
<li>
**把函数当成变量来用,关注描述问题而不是怎么实现,这样可以让代码更易读**
</li>
<li>
**因为函数返回里面的这个函数,所以函数关注的是表达式,关注的是描述这个问题,而不是怎么实现这个事情**
</li>
# Lisp 语言介绍
要说函数式语言不可避免地要说一下Lisp。
下面我们再来看看Scheme语言Lisp的一个方言的函数式玩法。在Scheme里所有的操作都是函数包括加减乘除这样的东西。所以一个表达式是这样的形式—— **(函数名 参数1 参数1**
```
(define (plus x y) (+ x y))
(define (times x y) (* x y))
(define (square x) (times x x))
```
上面三个函数:
- 用内置的 `+` 函数定义了一个新的 `plus` 函数。
- 用内置的 `*` 函数定义了一个新的 `times` 函数。
- 用之前的 `times` 函数定义了一个 `square` 函数。
下面这个函数定义了: f(x) = 5 * x^2 +10
```
(define (f1 x) ;;; f(x) = 5 * x^2 + 10
(plus 10 (times 5 (square x))))
```
也可以这样定义——使用 lambda 匿名函数。
```
(define f2
(lambda (x)
(define plus
(lambda (a b) (+ a b)))
(define times
(lambda (a b) (* a b)))
(plus 10 (times 5 (times x x)))))
```
在上面的这个代码里,我们使用 lambda 来定义函数 `f2` ,然后也同样用 lambda 定义了两个函数—— `plus``times`。 最后,由 `(plus 10 (times 5 (times x x)))` 定义了 `f2`
我们再来看一个阶乘的示例:
```
;;; recursion
(define factoral (lambda (x)
(if (&lt;= x 1) 1
(* x (factoral (- x 1))))))
(newline)
(display(factoral 6))
```
下面是另一个版本的,使用了尾递归。
```
;;; another version of recursion
(define (factoral_x n)
(define (iter product counter)
(if (&lt; counter n)
product
(iter (* counter product) (+ counter 1))))
(iter 1 1))
(newline)
(display(factoral_x 5))
```
# 函数式编程的思维方式
前面提到过多次函数式编程关注的是describe what to do, rather than how to do it。于是我们把以前的过程式编程范式叫做 Imperative Programming 指令式编程,而把函数式编程范式叫做 Declarative Programming 声明式编程。
## 传统方式的写法
下面我们看一下相关的示例。比如我们有3辆车比赛简单起见我们分别给这3辆车70%的概率让它们可以往前走一步一共有5次机会然后打出每一次这3辆车的前行状态。
对于Imperative Programming来说代码如下Python
```
from random import random
time = 5
car_positions = [1, 1, 1]
while time:
# decrease time
time -= 1
print ''
for i in range(len(car_positions)):
# move car
if random() &gt; 0.3:
car_positions[i] += 1
# draw car
print '-' * car_positions[i]
```
我们可以把这两重循环变成一些函数模块,这样有利于更容易地阅读代码:
```
from random import random
def move_cars():
for i, _ in enumerate(car_positions):
if random() &gt; 0.3:
car_positions[i] += 1
def draw_car(car_position):
print '-' * car_position
def run_step_of_race():
global time
time -= 1
move_cars()
def draw():
print ''
for car_position in car_positions:
draw_car(car_position)
time = 5
car_positions = [1, 1, 1]
while time:
run_step_of_race()
draw()
```
上面的代码,从主循环开始,我们可以很清楚地看到程序的主干,因为我们把程序的逻辑分成了几个函数。这样一来,代码逻辑就会变成几个小碎片,于是我们读代码时要考虑的上下文就少了很多,阅读代码也会更容易。不像第一个示例,如果没有注释和说明,你还是需要花些时间理解一下。而将代码逻辑封装成了函数后,我们就相当于给每个相对独立的程序逻辑取了个名字,于是代码成了自解释的。
但是,你会发现,封装成函数后,这些函数都会依赖于共享的变量来同步其状态。于是,在读代码的过程中,每当我们进入到函数里,读到访问了一个外部的变量时,我们马上要去查看这个变量的上下文,然后还要在大脑里推演这个变量的状态, 才能知道程序的真正逻辑。也就是说,这些函数必须知道其它函数是怎么修改它们之间的共享变量的,所以,这些函数是有状态的。
## 函数式的写法
我们知道,有状态并不是一件很好的事情,无论是对代码重用,还是对代码的并行来说,都是有副作用的。因此,要想个方法把这些状态搞掉,于是出现了函数式编程的编程范式。下面,我们来看看函数式的方式应该怎么写?
```
from random import random
def move_cars(car_positions):
return map(lambda x: x + 1 if random() &gt; 0.3 else x,
car_positions)
def output_car(car_position):
return '-' * car_position
def run_step_of_race(state):
return {'time': state['time'] - 1,
'car_positions': move_cars(state['car_positions'])}
def draw(state):
print ''
print '\n'.join(map(output_car, state['car_positions']))
def race(state):
draw(state)
if state['time']:
race(run_step_of_race(state))
race({'time': 5,
'car_positions': [1, 1, 1]})
```
上面的代码依然把程序的逻辑分成了函数。不过这些函数都是函数式的,它们有三个特点:它们之间没有共享的变量;函数间通过参数和返回值来传递数据;在函数里没有临时变量。
我们还可以看到for循环被递归取代了见race函数—— 递归是函数式编程中常用到的技术,正如前面所说的,递归的本质就是描述问题是什么。
# 函数式语言的三套件
函数式语言有三套件,**Map**、**Reduce** 和 **Filter**。这在谈C++的泛型编程时已经介绍过。下面我们来看一下Python语言中的一个示例。这个示例的需求是我们想把一个字符串数组中的字符串都转成小写。
用常规的面向过程的方式,代码如下所示:
```
# 传统的非函数式
upname =['HAO', 'CHEN', 'COOLSHELL']
lowname =[]
for i in range(len(upname)):
lowname.append( upname[i].lower() )
```
如果写成函数式,用 `map()` 函数,是下面这个样子。
```
# 函数式
def toUpper(item):
return item.upper()
upper_name = map(toUpper, [&quot;hao&quot;, &quot;chen&quot;, &quot;coolshell&quot;])
print upper_name
# 输出 ['HAO', 'CHEN', 'COOLSHELL']
```
顺便说一下上面的例子是不是和我们C++语言中的STL的`transform()`函数有些像?
```
string s=&quot;hello&quot;;
transform(s.begin(), s.end(), back_inserter(out), ::toupper);
```
在上面Python的那个例子中可以看到我们定义了一个函数toUpper这个函数没有改变传进来的值只是把传进来的值做个简单的操作然后返回。然后我们把它用在map函数中就可以很清楚地描述出我们想要干什么而不是去理解一个在循环中怎么实现的代码最终在读了很多循环的逻辑后才发现是什么意思。
如果你觉得上面的代码在传统的非函数式的方式下还是很容易读的,那么我们再来看一个计算数组平均值的代码:
```
# 计算数组中正数的平均值
num = [2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8]
positive_num_cnt = 0
positive_num_sum = 0
for i in range(len(num)):
if num[i] &gt; 0:
positive_num_cnt += 1
positive_num_sum += num[i]
if positive_num_cnt &gt; 0:
average = positive_num_sum / positive_num_cnt
print average
```
上面的代码如果没有注释的话,你需要看一会儿才能明白,只是计算数组中正数的平均值。
我们再来看看函数式下使用 filter/reduce 函数的玩法。
```
#计算数组中正数的平均值
positive_num = filter(lambda x: x&gt;0, num)
average = reduce(lambda x,y: x+y, positive_num) / len( positive_num )
```
首先,我们使用 filter 函数把正数过滤出来(注意: `lambda x : x&gt;0` 这个lambda表达式保存在一个新的数组中 —— `positive_num`。然后,我们使用 reduce 函数对数组 `positive_num` 求和后,再除以其个数,就得到正数的平均值了。
我们可以看到, **隐藏了数组遍历并过滤数组控制流程的 filter 和 reduce 不仅让代码更为简洁,因为代码里只有业务逻辑了,而且让我们能更容易地理解代码**
1.`num` 数组 `filter` 条件 `x &gt; 0` 的数据。
1. 然后对 `positive_num` 进行 `x + y` 操作的 reduce即求和。
1. ……
感觉代码更亲切了,不是吗?因为:
- 数据集、对数据的操作和返回值都放在了一起。
- 没有了循环体,就可以少了些临时用来控制程序执行逻辑的变量,也少了把数据倒来倒去的控制逻辑。
- **代码变成了在描述你要干什么,而不是怎么干**。
当然,如果你是第一次见到 map/reduce/filter那你可能还是会有点儿陌生和不解这只是你不了解罢了。
对于函数式编程的思路下图是一个比较形象的例子面包和蔬菜map到切碎的操作上再把结果给reduce成汉堡。
<img src="https://static001.geekbang.org/resource/image/cf/81/cfbbd404c980f98040514371aceb8881.png" alt="" />
在这个图中,**我们可以看到map和reduce不关心源输入数据它们只是控制并不是业务。控制是描述怎么干而业务是描述要干什么**。
# 函数式的pipeline模式
pipeline管道借鉴于Unix Shell的管道操作——把若干个命令串起来前面命令的输出成为后面命令的输入如此完成一个流式计算。管道绝对是一个伟大的发明它的设计哲学就是KISS 让每个功能就做一件事并把这件事做到极致软件或程序的拼装会变得更为简单和直观。这个设计理念影响非常深远包括今天的Web Service、云计算以及大数据的流式计算等。
比如我们如下的shell命令
```
ps auwwx | awk '{print $2}' | sort -n | xargs echo
```
上面的例子是要查看一个用户执行的进程列表列出来以后然后取第二列第二列是它的进程ID排个序再把它显示出来。
抽象成函数式的样子,我们就可以反过来,一层套一层。
```
xargs( echo, sort(n, awk('print $2', ps(auwwx))) )
```
我们也可以把函数放进数组里面,然后顺序执行一下。
```
pids = for_each(result, [ps_auwwx, awk_p2, sort_n, xargs_echo])
```
多说一句如果我们把这些函数比作微服务那么管道这个事是在干什么呢其实就是在做服务的编排。像Unix这些经典的技术上的实践或理论往往是可以反映到分布式架构的所以一般来说一个好的分布式架构师通常都是对这些传统的微观上的经典技术有非常深刻的认识因为这些东西在方法论上都是相通的。
好了还是让我们用一个简单的示例来看一下如何实现pipeline。
我们先来看一个程序这个程序的process()有三个步骤:
1. 找出偶数;
1. 乘以3
1. 转成字符串返回。
传统的非函数式的实现如下:
```
def process(num):
# filter out non-evens
if num % 2 != 0:
return
num = num * 3
num = 'The Number: %s' % num
return num
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for num in nums:
print process(num)
# 输出:
# None
# The Number: 6
# None
# The Number: 12
# None
# The Number: 18
# None
# The Number: 24
# None
# The Number: 30
```
我们可以看到输出的结果并不够完美另外代码阅读上如果没有注释你也会比较晕。下面我们来看看函数式的pipeline第一种方式应该怎么写
第一步,我们先把三个“子需求”写成函数:
```
def even_filter(nums):
for num in nums:
if num % 2 == 0:
yield num
def multiply_by_three(nums):
for num in nums:
yield num * 3
def convert_to_string(nums):
for num in nums:
yield 'The Number: %s' % num
```
然后,我们再把这三个函数串起来:
```
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pipeline = convert_to_string(multiply_by_three(even_filter(nums)))
for num in pipeline:
print num
# 输出:
# The Number: 6
# The Number: 12
# The Number: 18
# The Number: 24
# The Number: 30
```
上面我们动用了Python的关键字 yield它是一个类似 return 的关键字只是这个函数返回的是Generator生成器。所谓生成器指的是yield返回的是一个可迭代的对象并没有真正的执行函数。也就是说只有其返回的迭代对象被迭代时yield函数才会真正运行运行到yield语句时就会停住然后等下一次的迭代。 yield 是个比较诡异的关键字这就是lazy evluation懒惰加载
好了,根据前面的原则——“**使用Map &amp; Reduce不要使用循环**”还记得吗使用循环会让我们只能使用顺序型的数据结构那我们用比较纯朴的Map &amp; Reduce吧。
```
def even_filter(nums):
return filter(lambda x: x%2==0, nums)
def multiply_by_three(nums):
return map(lambda x: x*3, nums)
def convert_to_string(nums):
return map(lambda x: 'The Number: %s' % x, nums)
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pipeline = convert_to_string(
multiply_by_three(
even_filter(nums)
)
)
for num in pipeline:
print num
```
上面的代码是不是更容易读了,但需要嵌套使用函数,这个有点儿令人不爽,如果我们能像下面这个样子就好了(第二种方式)。
```
pipeline_func(nums, [even_filter,
multiply_by_three,
convert_to_string])
```
可以看到其实就是对一堆函数做一个reduce 于是pipeline函数可以实现成下面这样
```
def pipeline_func(data, fns):
return reduce(lambda a, x: x(a), fns, data)
```
当然使用Python的 `force` 函数以及decorator模式可以把上面的代码写得更像管道
```
class Pipe(object):
def __init__(self, func):
self.func = func
def __ror__(self, other):
def generator():
for obj in other:
if obj is not None:
yield self.func(obj)
return generator()
@Pipe
def even_filter(num):
return num if num % 2 == 0 else None
@Pipe
def multiply_by_three(num):
return num*3
@Pipe
def convert_to_string(num):
return 'The Number: %s' % num
@Pipe
def echo(item):
print item
return item
def force(sqs):
for item in sqs: pass
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
force(nums | even_filter | multiply_by_three | convert_to_string | echo)
```
# 小结
相对于计算机发展史函数式编程是个非常古老的概念它的核心思想是将运算过程尽量写成一系列嵌套的函数调用关注的是做什么而不是怎么做因而被称为声明式编程。以Stateless无状态和Immutable不可变为主要特点代码简洁易于理解能便于进行并行执行易于做代码重构函数执行没有顺序上的问题支持惰性求值具有函数的确定性——无论在什么场景下都会得到同样的结果。
本文结合递归、map和reduce以及pipeline等技术对比了非函数式编程和函数式编程在解决相同问题时的不同处理思路让你对函数式编程范式有了清晰明确的认知。并在文末引入了decorator修饰器使得将普通函数管道化成为一件轻而易举的事情。此时你可能有疑问decorator到底是什么呢怎样使用它呢敬请关注下一章中的内容来得到这些答案。
了解了这么多函数式编程的知识,想请你深入思考一个问题:你是偏好在命令式编程语言中使用函数式编程风格呢,还是坚持使用函数式语言编程?原因是什么?欢迎在评论区留言和我一起探讨。
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)
<img src="https://static001.geekbang.org/resource/image/40/18/40341574317cc135385c6946a17d2818.jpg" alt="" /></a>
[戳此获取你的专属海报](https://time.geekbang.org/activity/sale-poster?utm_source=geektime&amp;utm_medium=chenhao&amp;utm_campaign=201803&amp;utm_content=chenhaofxbanner)

View File

@@ -0,0 +1,498 @@
在上一篇文章中我们领略了函数式编程的趣味和魅力主要讲了函数式编程的主要技术。还记得有哪些吗递归、Map、Reduce、Filter等并利用Python的Decorator和Generator功能将多个函数组合成了管道。
此时你心中可能会有个疑问这个decorator又是怎样工作的呢这就是本文中要讲述的内容“Decorator模式”又叫“修饰器模式”或是“装饰器模式”。
# Python的Decorator
Python的Decorator在使用上和Java的Annotation以及C#的Attribute)很相似,就是在方法名前面加一个@XXX注解来为这个方法装饰一些东西。但是Java/C#的Annotation也很让人望而却步太过于复杂了。你要玩它需要先了解一堆Annotation的类库文档感觉几乎就是在学另外一门语言。
而Python使用了一种相对于Decorator Pattern和Annotation来说非常优雅的方法这种方法不需要你去掌握什么复杂的OO模型或是Annotation的各种类库规定完全就是语言层面的玩法一种函数式编程的技巧。
这是我最喜欢的一个模式了,也是一个挺好玩儿的东西,这个模式动用了函数式编程的一个技术——用一个函数来构造另一个函数。
好了我们先来点感性认识看一个Python修饰器的Hello World代码。
```
def hello(fn):
def wrapper():
print &quot;hello, %s&quot; % fn.__name__
fn()
print &quot;goodbye, %s&quot; % fn.__name__
return wrapper
@hello
def Hao():
print &quot;i am Hao Chen&quot;
Hao()
```
代码的执行结果如下:
```
$ python hello.py
hello, Hao
i am Hao Chen
goodbye, Hao
```
你可以看到如下的东西:
<li>
函数 `Hao` 前面有个@hello的“注解”,`hello` 就是我们前面定义的函数 `hello`
</li>
<li>
`hello` 函数中,其需要一个 `fn` 的参数(这就是用来做回调的函数);
</li>
<li>
hello函数中返回了一个inner函数 `wrapper`,这个 `wrapper`函数回调了传进来的 `fn`,并在回调前后加了两条语句。
</li>
对于Python的这个@注解语法糖Syntactic sugar来说当你在用某个@decorator来修饰某个函数 `func` 时,如下所示:
```
@decorator
def func():
pass
```
其解释器会解释成下面这样的语句:
```
func = decorator(func)
```
这不就是把一个函数当参数传到另一个函数中然后再回调吗是的。但是我们需要注意那里还有一个赋值语句把decorator这个函数的返回值赋值回了原来的 `func`
我们再来看一个带参数的玩法:
```
def makeHtmlTag(tag, *args, **kwds):
def real_decorator(fn):
css_class = &quot; class='{0}'&quot;.format(kwds[&quot;css_class&quot;]) \
if &quot;css_class&quot; in kwds else &quot;&quot;
def wrapped(*args, **kwds):
return &quot;&lt;&quot;+tag+css_class+&quot;&gt;&quot; + fn(*args, **kwds) + &quot;&lt;/&quot;+tag+&quot;&gt;&quot;
return wrapped
return real_decorator
@makeHtmlTag(tag=&quot;b&quot;, css_class=&quot;bold_css&quot;)
@makeHtmlTag(tag=&quot;i&quot;, css_class=&quot;italic_css&quot;)
def hello():
return &quot;hello world&quot;
print hello()
# 输出:
# &lt;b class='bold_css'&gt;&lt;i class='italic_css'&gt;hello world&lt;/i&gt;&lt;/b&gt;
```
在上面这个例子中,我们可以看到:`makeHtmlTag`有两个参数。所以,为了让 `hello = makeHtmlTag(arg1, arg2)(hello)` 成功, `makeHtmlTag` 必需返回一个decorator这就是为什么我们在 `makeHtmlTag` 中加入了 `real_decorator()`)。
这样一来我们就可以进入到decorator的逻辑中去了——decorator得返回一个wrapperwrapper里回调 `hello`。看似那个 `makeHtmlTag()` 写得层层叠叠,但是,已经了解了本质的我们觉得写得很自然。
我们再来看一个为其它函数加缓存的示例:
```
from functools import wraps
def memoization(fn):
cache = {}
miss = object()
@wraps(fn)
def wrapper(*args):
result = cache.get(args, miss)
if result is miss:
result = fn(*args)
cache[args] = result
return result
return wrapper
@memoization
def fib(n):
if n &lt; 2:
return n
return fib(n - 1) + fib(n - 2)
```
上面这个例子中是一个斐波那契数例的递归算法。我们知道这个递归是相当没有效率的因为会重复调用。比如我们要计算fib(5),于是其分解成 `fib(4) + fib(3)`,而 `fib(4)` 分解成 `fib(3) + fib(2)``fib(3)` 又分解成`fib(2) + fib(1)`……你可以看到,基本上来说,`fib(3)``fib(2)``fib(1)`在整个递归过程中被调用了至少两次。
而我们用decorator在调用函数前查询一下缓存如果没有才调用有了就从缓存中返回值。一下子这个递归从二叉树式的递归成了线性的递归。`wraps` 的作用是保证 `fib` 的函数名不被 `wrapper` 所取代。
除此之外Python还支持类方式的decorator。
```
class myDecorator(object):
def __init__(self, fn):
print &quot;inside myDecorator.__init__()&quot;
self.fn = fn
def __call__(self):
self.fn()
print &quot;inside myDecorator.__call__()&quot;
@myDecorator
def aFunction():
print &quot;inside aFunction()&quot;
print &quot;Finished decorating aFunction()&quot;
aFunction()
# 输出:
# inside myDecorator.__init__()
# Finished decorating aFunction()
# inside aFunction()
# inside myDecorator.__call__()
```
上面这个示例展示了用类的方式声明一个decorator。我们可以看到这个类中有两个成员
1. 一个是`__init__()`这个方法是在我们给某个函数decorate时被调用所以需要有一个 `fn` 的参数也就是被decorate的函数。
1. 一个是`__call__()`这个方法是在我们调用被decorate的函数时被调用的。
从上面的输出中,可以看到整个程序的执行顺序,这看上去要比“函数式”的方式更易读一些。
我们来看一个实际点的例子下面这个示例展示了通过URL的路由来调用相关注册的函数示例
```
class MyApp():
def __init__(self):
self.func_map = {}
def register(self, name):
def func_wrapper(func):
self.func_map[name] = func
return func
return func_wrapper
def call_method(self, name=None):
func = self.func_map.get(name, None)
if func is None:
raise Exception(&quot;No function registered against - &quot; + str(name))
return func()
app = MyApp()
@app.register('/')
def main_page_func():
return &quot;This is the main page.&quot;
@app.register('/next_page')
def next_page_func():
return &quot;This is the next page.&quot;
print app.call_method('/')
print app.call_method('/next_page')
```
注意上面这个示例中decorator类不是真正的decorator其中也没有`__call__()`并且wrapper返回了原函数。所以原函数没有发生任何变化。
# Go语言的Decorator
Python有语法糖所以写出来的代码比较酷。但是对于没有修饰器语法糖这类语言写出来的代码会是怎么样的我们来看一下Go语言的代码。
还是从一个Hello World开始。
```
package main
import &quot;fmt&quot;
func decorator(f func(s string)) func(s string) {
return func(s string) {
fmt.Println(&quot;Started&quot;)
f(s)
fmt.Println(&quot;Done&quot;)
}
}
func Hello(s string) {
fmt.Println(s)
}
func main() {
decorator(Hello)(&quot;Hello, World!&quot;)
}
```
可以看到,我们动用了一个高阶函数 `decorator()`,在调用的时候,先把 `Hello()` 函数传进去,然后其返回一个匿名函数。这个匿名函数中除了运行了自己的代码,也调用了被传入的 `Hello()` 函数。
这个玩法和Python的异曲同工只不过Go并不支持像Python那样的@decorator语法糖。所以,在调用上有些难看。当然,如果要想让代码容易读一些,你可以这样:
```
hello := decorator(Hello)
hello(&quot;Hello&quot;)
```
我们再来看一个为函数log消耗时间的例子
```
type SumFunc func(int64, int64) int64
func getFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func timedSumFunc(f SumFunc) SumFunc {
return func(start, end int64) int64 {
defer func(t time.Time) {
fmt.Printf(&quot;--- Time Elapsed (%s): %v ---\n&quot;,
getFunctionName(f), time.Since(t))
}(time.Now())
return f(start, end)
}
}
func Sum1(start, end int64) int64 {
var sum int64
sum = 0
if start &gt; end {
start, end = end, start
}
for i := start; i &lt;= end; i++ {
sum += i
}
return sum
}
func Sum2(start, end int64) int64 {
if start &gt; end {
start, end = end, start
}
return (end - start + 1) * (end + start) / 2
}
func main() {
sum1 := timedSumFunc(Sum1)
sum2 := timedSumFunc(Sum2)
fmt.Printf(&quot;%d, %d\n&quot;, sum1(-10000, 10000000), sum2(-10000, 10000000))
}
```
关于上面的代码:
<li>
有两个 Sum 函数,`Sum1()` 函数就是简单地做个循环,`Sum2()` 函数动用了数据公式。(注意:`start``end` 有可能有负数的情况。)
</li>
<li>
代码中使用了Go语言的反射机制来获取函数名。
</li>
<li>
修饰器函数是 `timedSumFunc()`
</li>
再来看一个 HTTP 路由的例子:
```
func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(&quot;---&gt;WithServerHeader()&quot;)
w.Header().Set(&quot;Server&quot;, &quot;HelloServer v0.0.1&quot;)
h(w, r)
}
}
func WithAuthCookie(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(&quot;---&gt;WithAuthCookie()&quot;)
cookie := &amp;http.Cookie{Name: &quot;Auth&quot;, Value: &quot;Pass&quot;, Path: &quot;/&quot;}
http.SetCookie(w, cookie)
h(w, r)
}
}
func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(&quot;---&gt;WithBasicAuth()&quot;)
cookie, err := r.Cookie(&quot;Auth&quot;)
if err != nil || cookie.Value != &quot;Pass&quot; {
w.WriteHeader(http.StatusForbidden)
return
}
h(w, r)
}
}
func WithDebugLog(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(&quot;---&gt;WithDebugLog&quot;)
r.ParseForm()
log.Println(r.Form)
log.Println(&quot;path&quot;, r.URL.Path)
log.Println(&quot;scheme&quot;, r.URL.Scheme)
log.Println(r.Form[&quot;url_long&quot;])
for k, v := range r.Form {
log.Println(&quot;key:&quot;, k)
log.Println(&quot;val:&quot;, strings.Join(v, &quot;&quot;))
}
h(w, r)
}
}
func hello(w http.ResponseWriter, r *http.Request) {
log.Printf(&quot;Received Request %s from %s\n&quot;, r.URL.Path, r.RemoteAddr)
fmt.Fprintf(w, &quot;Hello, World! &quot;+r.URL.Path)
}
```
上面的代码中我们写了多个函数。有写HTTP响应头的有写认证Cookie的有检查认证Cookie的有打日志的……在使用过程中我们可以把其嵌套起来使用在修饰过的函数上继续修饰这样就可以拼装出更复杂的功能。
```
func main() {
http.HandleFunc(&quot;/v1/hello&quot;, WithServerHeader(WithAuthCookie(hello)))
http.HandleFunc(&quot;/v2/hello&quot;, WithServerHeader(WithBasicAuth(hello)))
http.HandleFunc(&quot;/v3/hello&quot;, WithServerHeader(WithBasicAuth(WithDebugLog(hello))))
err := http.ListenAndServe(&quot;:8080&quot;, nil)
if err != nil {
log.Fatal(&quot;ListenAndServe: &quot;, err)
}
}
```
当然如果一层套一层不好看的话我们可以使用pipeline的玩法需要先写一个工具函数——用来遍历并调用各个decorator
```
type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc
func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
for i := range decors {
d := decors[len(decors)-1-i] // iterate in reverse
h = d(h)
}
return h
}
```
然后,我们就可以像下面这样使用了。
```
http.HandleFunc(&quot;/v4/hello&quot;, Handler(hello,
WithServerHeader, WithBasicAuth, WithDebugLog))
```
这样的代码是不是更易读了一些pipeline的功能也就出来了。
不过对于Go的修饰器模式还有一个小问题——好像无法做到泛型就像上面那个计算时间的函数一样它的代码耦合了需要被修饰的函数的接口类型无法做到非常通用。如果这个事解决不了那么这个修饰器模式还是有点不好用的。
因为Go语言不像Python和JavaPython是动态语言而Java有语言虚拟机所以它们可以干许多比较变态的事儿然而Go语言是一个静态的语言这意味着其类型需要在编译时就要搞定否则无法编译。不过Go语言支持的最大的泛型是interface{}还有比较简单的Reflection机制在上面做做文章应该还是可以搞定的。
废话不说下面是我用Reflection机制写的一个比较通用的修饰器为了便于阅读我删除了出错判断代码
```
func Decorator(decoPtr, fn interface{}) (err error) {
var decoratedFunc, targetFunc reflect.Value
decoratedFunc = reflect.ValueOf(decoPtr).Elem()
targetFunc = reflect.ValueOf(fn)
v := reflect.MakeFunc(targetFunc.Type(),
func(in []reflect.Value) (out []reflect.Value) {
fmt.Println(&quot;before&quot;)
out = targetFunc.Call(in)
fmt.Println(&quot;after&quot;)
return
})
decoratedFunc.Set(v)
return
}
```
上面的代码动用了 `reflect.MakeFunc()` 函数制作出了一个新的函数,其中的 `targetFunc.Call(in)` 调用了被修饰的函数。关于Go语言的反射机制推荐官方文章——《[The Laws of Reflection](https://blog.golang.org/laws-of-reflection)》,在这里我不多说了。
上面这个 `Decorator()` 需要两个参数:
- 第一个是出参 `decoPtr` ,就是完成修饰后的函数。
- 第二个是入参 `fn` ,就是需要修饰的函数。
这样写是不是有些二的确是的。不过这是我个人在Go语言里所能写出来的最好的代码了。如果你知道更优雅的写法请你一定告诉我
好的,让我们来看一下使用效果。首先,假设我们有两个需要修饰的函数:
```
func foo(a, b, c int) int {
fmt.Printf(&quot;%d, %d, %d \n&quot;, a, b, c)
return a + b + c
}
func bar(a, b string) string {
fmt.Printf(&quot;%s, %s \n&quot;, a, b)
return a + b
}
```
然后,我们可以这样做:
```
type MyFoo func(int, int, int) int
var myfoo MyFoo
Decorator(&amp;myfoo, foo)
myfoo(1, 2, 3)
```
你会发现,使用 `Decorator()` 时,还需要先声明一个函数签名,感觉好傻啊。一点都不泛型,不是吗?谁叫这是有类型的静态编译的语言呢?
嗯。如果你不想声明函数签名,那么也可以这样:
```
mybar := bar
Decorator(&amp;mybar, bar)
mybar(&quot;hello,&quot;, &quot;world!&quot;)
```
好吧看上去不是那么得漂亮但是it does work。看样子Go语言目前本身的特性无法做成像Java或Python那样对此我们只能多求Go语言多放糖了
# 小结
好了,讲了那么多的例子,看了那么多的代码,我估计你可能有点晕,让我们来做个小结吧。
通过上面Python和Go修饰器的例子我们可以看到所谓的修饰器模式其实是在做下面的几件事。
<li>
表面上看,修饰器模式就是扩展现有的一个函数的功能,让它可以干一些其他的事,或是在现有的函数功能上再附加上一些别的功能。
</li>
<li>
除了我们可以感受到**函数式编程**下的代码扩展能力,我们还能感受到函数的互相和随意拼装带来的好处。
</li>
<li>
但是深入看一下我们不难发现Decorator这个函数其实是可以修饰几乎所有的函数的。于是这种可以通用于其它函数的编程方式可以很容易地将一些非业务功能的、属于控制类型的代码给抽象出来所谓的控制类型的代码就是像for-loop或是打日志或是函数路由或是求函数运行时间之类的非业务功能性的代码
</li>
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)

View File

@@ -0,0 +1,328 @@
前面我们谈了函数式编程,函数式编程总结起来就是把一些功能或逻辑代码通过函数拼装方式来组织的玩法。这其中涉及最多的是函数,也就是编程中的代码逻辑。但我们知道,代码中还是需要处理数据的,这些就是所谓的“状态”,函数式编程需要我们写出无状态的代码。
而这天下并不存在没有状态没有数据的代码,如果函数式编程不处理状态这些东西,那么,状态会放在什么地方呢?总是需要一个地方放这些数据的。
对于状态和数据的处理我们有必要提一下“面向对象编程”Object-oriented programmingOOP这个编程范式了。我们知道**面向对象的编程有三大特性:封装、继承和多态**。
面向对象编程是一种具有对象概念的程序编程范型,同时也是一种程序开发的抽象方针,它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的可重用性、灵活性和可扩展性,对象里的程序可以访问及修改对象相关联的数据。在面向对象编程里,计算机程序会被设计成彼此相关的对象。
面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对计算机下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。
目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。
现在几乎所有的主流语言都支持面向对象比如Common Lisp、Python、C++、Objective-C、Smalltalk、Delphi、Java、Swift、C#、Perl、Ruby与PHP等。
说起面向对象就不得不提由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides合作出版的《[设计模式:可复用面向对象软件的基础](https://book.douban.com/subject/1052241/)》Design Patterns - Elements of Reusable Object-Oriented Software一书在此书中共收录了23种设计模式。
这本书的23个经典的设计模式基本上就是说了两个面向对象的核心理念
<li>**&quot;Program to an interface, not an implementation.&quot;**
<ul>
- 使用者不需要知道数据类型、结构、算法的细节。
- 使用者不需要知道实现细节,只需要知道提供的接口。
- 利于抽象、封装、动态绑定、多态。
- 符合面向对象的特质和理念。
- 继承需要给子类暴露一些父类的设计和实现细节。
- 父类实现的改变会造成子类也需要改变。
- 我们以为继承主要是为了代码重用,但实际上在子类中需要重新实现很多父类的方法。
- 继承更多的应该是为了多态。
# 示例一:拼装对象
好,我们先来看一个示例,假设我们有如下的描述:
- **四个物体**:木头桌子、木头椅子、塑料桌子、塑料椅子
- **四个属性**:燃点、密度、价格、重量
那么,我们怎么用面向对象的方式来设计我们的类呢?
参看下图:
<img src="https://static001.geekbang.org/resource/image/21/b7/21f0377bc34b52e5c007a3f7c76054b7.png" alt="" />
- 图的左边是“材质类” Material。其属性有燃点和密度。
- 图的右边是“家具类” Furniture。其属性有价格和体积。
- 在Furniture中耦合了Material。而具体的Material是Wood还是Plastic这在构造对象的时候注入到Furniture里就好了。
- 这样,在家具类中,通过材料的密度属性和家具的体积属性就可以计算出重量属性。
这样设计的优点显而易见它能和现实世界相对应起来而且材料类是可以重用的。这个模式也表现了面向对象的拼装数据的另一个精髓——喜欢组合而不是继承。这个模式在设计模式里叫“桥接Bridge模式”。
和函数式编程来比较,函数式强调动词,而面向对象强调名词,面向对象更多的关注接口间的关系,而通过多态来适配不同的具体实现。
# 示例二:拼装功能
再来看一个示例。我们的需求是:处理电商系统中的订单,处理订单有一个关键的动作就是计算订单的价格。有的订单需要打折,有的则不打折。
在进行面向对象编程时假设我们用Java语言我们需要先写一个接口—— `BillingStrategy`,其中一个方法就是`GetActPrice(double rawPrice)`,输入一个原始的价格,输出一个根据相应的策略计算出来的价格。
```
interface BillingStrategy {
public double GetActPrice(double rawPrice);
}
```
这个接口很简单,只是对接口的抽象,而与实现无关。现在我们需要对这个接口进行实现。
```
// Normal billing strategy (unchanged price)
class NormalStrategy implements BillingStrategy {
@Override
public double GetActPrice(double rawPrice) {
return rawPrice;
}
}
// Strategy for Happy hour (50% discount)
class HappyHourStrategy implements BillingStrategy {
@Override
public double GetActPrice(double rawPrice) {
return rawPrice * 0.5;
}
}
```
上面的代码实现了两个策略,一个是不打折的:`NormalStrategy`一个是打了5折的`HappyHourStrategy`
于是,我们先封装订单项 `OrderItem`,其包含了每个商品的原始价格和数量,以及计算价格的策略。
```
class OrderItem {
public String Name;
public double Price;
public int Quantity;
public BillingStrategy Strategy;
public OrderItem(String name, double price, int quantity, BillingStrategy strategy) {
this.Name = name;
this.Price = price;
this.Quantity = quantity;
this.Strategy = strategy;
}
}
```
然后,在我们的订单类—— `Order` 中封装了 `OrderItem` 的列表,即商品列表。并在操作订单添加购买商品时,加入一个计算价格的 `BillingStrategy`
```
class Order {
private List&lt;OrderItem&gt; orderItems = new ArrayList&lt;OrderItem&gt;();
private BillingStrategy strategy = new NormalStrategy();
public void Add(String name, double price, int quantity, BillingStrategy strategy) {
orderItems.add(new OrderItem(name, price, quantity, strategy));
}
// Payment of bill
public void PayBill() {
double sum = 0;
for (OrderItem item : orderItems) {
actPrice = item.Strategy.GetActPrice(item.price * item.quantity);
sum += actPrice;
System.out.println(&quot;%s -- %f(%d) - %f&quot;,
item.name, item.price, item.quantity, actPrice);
}
System.out.println(&quot;Total due: &quot; + sum);
}
}
```
最终,我们在 `PayBill()` 函数中,把整个订单的价格明细和总价打印出来。
在上面这个示例中,可以看到,我把定价策略和订单处理的流程分开了。这么做的好处是,我们可以随时给不同的商品注入不同的价格计算策略,这样一来就有很高的灵活度了。剩下的事就交给我们的运营人员来配置不同的商品使用什么样的价格计算策略了。
注意现实社会中订单价格计算会比这个事复杂得多比如有会员价有打折卡还有商品的打包价等而且还可以叠加不同的策略叠加策略用前面说的函数式的pipeline或decorator就可以实现。我们这里只是为了说明面向对象编程范式所以故意简单化了。
其实,这个设计模式叫——策略模式。我认为,这是设计模式中最为经典的模式了,其充分体现了面向对象编程的方式。
# 示例三:资源管理
先看一段代码:
```
mutex m;
void foo() {
m.lock();
Func();
if ( ! everythingOk() ) return;
...
...
m.unlock();
}
```
可以看到,上面这段代码是有问题的,原因是:那个 `if` 语句返回时没有把锁给unlock掉这会导致锁没有被释放。如果我们要把代码写对需要在return前unlock一下。
```
mutex m;
void foo() {
m.lock();
Func();
if ( ! everythingOk() ) {
m.unlock();
return;
}
...
...
m.unlock();
}
```
但是,在所有的函数退出的地方都要加上 `m.unlock();` 语句,这会让我们很难维护代码。于是可以使用面向对象的编程模式,我们先设计一个代理类。
```
class lock_guard {
private:
mutex &amp;_m;
public:
lock_guard(mutex &amp;m):_m(m) { _m.lock(); }
~lock_guard() { _m.unlock(); }
};
```
然后,我们的代码就可以这样写了:
```
mutex m;
void foo() {
lock_guard guard(m);
Func();
if ( ! everythingOk() ) {
return;
}
...
...
}
```
这个技术叫RAIIResource Acquisition Is Initialization资源获取就是初始化 是C++中的一个利用了面向对象的技术。这个设计模式叫“代理模式”。我们可以把一些控制资源分配和释放的逻辑交给这些代理类,然后,只需要关注业务逻辑代码了。而且,在我们的业务逻辑代码中,减少了这些和业务逻辑不相关的程序控制的代码。
从上面的代码中,我们可以看到下面几个面向对象的事情。
<li>
我们使用接口抽象了具体的实现类。
</li>
<li>
然后其它类耦合的是接口而不是实现类。这就是多态,其增加了程序的可扩展性。
</li>
<li>
因为这就是接口编程所谓接口也就是一种“协议”就像HTTP协议一样。浏览器和后端的程序都依赖于这一种协议而不是具体实现如果是依赖具体实现那么浏览器就要依赖后端的编程语言或中间件了这就太恶心了。于是浏览器和后端的程序就完全解除依赖关系而去依赖于一个标准的协议。
</li>
<li>
这就是面向对象的编程范式的精髓同样也是IoC/DIP控制反转/依赖倒置)的本质。
</li>
# IoC 控制反转
关于IoC的的概念提出来已经很多年了其被用于一种面向对象的设计。我在这里再简单地回顾一下这个概念。我先谈技术再说管理。
话说,我们有一个开关要控制一个灯的开和关这两个动作,最常见也是最没有技术含量的实现会是这个样子:
<img src="https://static001.geekbang.org/resource/image/60/ca/6095b6ad1e168cb3bd973bf41489b1ca.jpg" alt="" />
然后,有一天,我们发现需要对灯泡扩展一下,于是做了个抽象类:
<img src="https://static001.geekbang.org/resource/image/9f/c3/9f8d0a147a15fe6c0273796bedce1dc3.jpg" alt="" />
但是,如果有一天,我们发现这个开关可能还要控制别的不单单是灯泡的东西,就会发现这个开关耦合了灯泡这种类别,非常不利于扩展,于是反转控制出现了。
就像现实世界一样造开关的工厂根本不关心要控制的东西是什么它只做一个开关应该做好的事就是把电接通把电断开不管是手动的还是声控的还是光控还是遥控的。而我们造的各种各样的灯泡不管是日光灯、白炽灯的工厂也不关心你用什么样的开关反正我只管把灯的电源接口给做出来。然后开关厂和电灯厂依赖于一个标准的通电和断电的接口。于是产生了IoC控制反转如下图
<img src="https://static001.geekbang.org/resource/image/4d/13/4d1b95052b62dc82dc099302c8612613.jpg" alt="" />
所谓控制反转的意思是,开关从以前设备的专用开关,转变到了控制电源的开关,而以前的设备要反过来依赖于开关厂声明的电源连接接口。只要符合开关厂定义的电源连接的接口,这个开关可以控制所有符合这个电源连接接口的设备。也就是说,开关从依赖设备这种情况,变成了设备反过来依赖于开关所定义的接口。
这样的例子在生活中太多见了,比如说:
<li>
钱就是一个很好的例子。以前大家都是“以物易物”所以在各种物品之前都需要相应的“交易策略”比如一头羊换2袋米一袋米换一斤猪后腿肉……这种换算太复杂了。于是“钱”就出来了所谓“钱”其实就是一种交易协议所有的商品都依赖这个协议而不用再互相依赖了。于是整个世界的运作就简单了很多。
</li>
<li>
在交易的过程中,卖家向买家卖东西,一手交钱一手交货,所以,基本上来说卖家和买家必需强耦合(必需见面)。这个时候,银行出来做担保,买家把钱先垫到银行,银行让卖家发货,买家验货后,银行再把钱打给卖家。这就是反转控制。买卖双方把对对方的直接依赖和控制,反转到了让对方来依赖一个标准的交易模型的接口。股票交易也是一样的,证交所就是买卖双方的标准交易模型接口。
</li>
<li>
上面这个例子,可能还不明显,再举一个例子。海尔公司作为一个电器制商需要把自己的商品分销到全国各地,但是发现,不同的分销渠道有不同的玩法,于是派出了各种销售代表玩不同的玩法。随着渠道越来越多,发现,每增加一个渠道就要新增一批人和一个新的流程,严重耦合并依赖各渠道商的玩法。
</li>
实在受不了了,于是制定业务标准,开发分销信息化系统,只有符合这个标准的渠道商才能成为海尔的分销商,让各个渠道商反过来依赖自己标准。反转了控制,倒置了依赖。
这个思维方式其实还深远地影响了很多东西,比如我们的系统架构。
- 云计算平台中有很多的云产品线。一些底层服务的开发团队只管开发底层的技术,然后什么也不管了,就交给上层的开发人员。上层开发人员在底层团队开发出来的产品上面开发各种管理这个底层资源的东西,比如:生产底层资源的业务,底层资源的控制台,底层资源的监控系统。
然而,随着接入的资源越来越多,上层为各个云资源控制生产,开发控制台和监控的团队,完全干不过来了。这个时候依赖倒置和反转控制又可以解决问题了。为了有统一体验,各个云产品线需要遵从一定的协议或规范来开发。比如,每个云产品团队需要按照标准定义相关资源的生命周期管理,提供控制台,接入整体监控系统,通过标准的协议开发控制系统。
- 集中式处理电子商务订单的流程。各个垂直业务线都需要通过这个平台来处理自己的交易业务,但是垂直业务线上的个性化需求太多。于是,这个技术平台开始发现,对来自各个业务方的需求应接不暇,各种变态需求严重干扰系统,各种技术决策越来越不好做,导致需求排期排不过来。
这个时候也可以使用依赖倒置和反转控制的思想来解决问题开发一个插件模型、工作流引擎和Pub/Sub系统让业务方的个性化需求支持以插件的方式插入订单流程中。业务方自己的数据存在自己的库中业务逻辑也不要侵入系统并可以使用工作流引擎或Pub/Sub的协议标准来自己定义工作流的各个步骤甚至把工作流引擎的各个步骤的decider交给各个业务方自行处理
让各个业务方来依赖于标准插件和工作流接口,反转控制,让它们来控制系统,依赖倒置,让它们来依赖标准。
上面这些我想说什么?我想说的是:
<li>
我们每天都在标准化和定制化中纠结。我们痛苦于哪些应该是平台要做的,哪些应该要甩出去的。
</li>
<li>
这里面会出现大量的与业务无关的软件或中间件,包括协议、数据、接口……
</li>
<li>
通过面向对象的这些方式,我们可以通过抽象来解耦,通过中间件来解耦,这样可以降低软件的复杂度。
</li>
总而言之,我们就是想通过一种标准来让业务更为规范。
# 小结
不过,我们也需要知道面向对象的优缺点。
**优点**
- 能和真实的世界交相辉映,符合人的直觉。
- 面向对象和数据库模型设计类型,更多地关注对象间的模型设计。
- 强调于“名词”而不是“动词”,更多地关注对象和对象间的接口。
- 根据业务的特征形成一个个高内聚的对象,有效地分离了抽象和具体实现,增强了可重用性和可扩展性。
- 拥有大量非常优秀的设计原则和设计模式。
- S.O.L.I.D单一功能、开闭原则、里氏替换、接口隔离以及依赖反转是面向对象设计的五个基本原则、IoC/DIP……
**缺点**
- 代码都需要附着在一个类上,从一侧面上说,其鼓励了类型。
- 代码需要通过对象来达到抽象的效果,导致了相当厚重的“代码粘合层”。
- 因为太多的封装以及对状态的鼓励,导致了大量不透明并在并发下出现很多问题。
还是好多人并不是喜欢面向对象,尤其是喜欢函数式和泛型那些人,似乎都是非常讨厌面向对象的。
通过对象来达到抽象结果,把代码分散在不同的类里面,然后,要让它们执行起来,就需要把这些类粘合起来。所以,它另外一方面鼓励相当厚重的代码黏合层(代码黏合层就是把代码黏合到这里面)。
在Java里有很多注入方式像Spring那些注入鼓励黏合导致了大量的封装完全不知道里面在干什么事情。而且封装屏蔽了细节具体发生啥事你还不知道。这些都是面向对象不太好的地方。
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)

View File

@@ -0,0 +1,327 @@
基于原型Prototype的编程其实也是面向对象编程的一种方式。没有class化的直接使用对象。又叫基于实例的编程。其主流的语言就是JavaScript与传统的面对象编程的比较如下
<li>
在基于类的编程当中,对象总共有两种类型。类定义了对象的基本布局和函数特性,而接口是“可以使用的”对象,它基于特定类的样式。在此模型中,类表现为行为和结构的集合,对所有接口来说这些类的行为和结构都是相同的。因而,区分规则首先是基于行为和结构,而后才是状态。
</li>
<li>
原型编程的主张者经常争论说,基于类的语言提倡使用一个关注分类和类之间关系的开发模型。与此相对,原型编程看起来提倡程序员关注一系列对象实例的行为,而之后才关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是分成类。
</li>
因为如此很多基于原型的系统提倡运行时进行原型的修改而只有极少数基于类的面向对象系统比如第一个动态面向对象的系统Smalltalk允许类在程序运行时被修改。
<li>
在基于类的语言中,一个新的实例通过类构造器和构造器可选的参数来构造,结果实例由类选定的行为和布局创建模型。
</li>
<li>
在基于原型的系统中构造对象有两种方法通过复制已有的对象或者通过扩展空对象创建。很多基于原型的系统提倡运行时进行原型的修改而基于类的面向对象系统只有动态语言允许类在运行时被修改Common Lisp、Dylan、Objective-C、Perl、Python、Ruby和Smalltalk
</li>
# JavaScript的原型概念
这里我们主要以JavaScript举例面向对象里面要有个Class。但是JavaScript觉得不是这样的它就是要基于原型编程就不要Class就直接在对象上改就行了基于编程的修改直接对类型进行修改。
我们先来看一个示例。
```
var foo = {name: &quot;foo&quot;, one: 1, two: 2};
var bar = {three: 3};
```
每个对象都有一个 `__proto__` 的属性,这个就是“原型”。对于上面的两个对象,如果我们把 `foo` 赋值给 `bar.__proto__`,那就意味着,`bar` 的原型就成了 `foo`的。
```
bar.__proto__ = foo; // foo is now the prototype of bar.
```
于是,我们就可以在 `bar` 里面访问 `foo` 的属性了。
```
// If we try to access foo's properties from bar
// from now on, we'll succeed.
bar.one // Resolves to 1.
// The child object's properties are also accessible.
bar.three // Resolves to 3.
// Own properties shadow prototype properties
bar.name = &quot;bar&quot;;
foo.name; // unaffected, resolves to &quot;foo&quot;
bar.name; // Resolves to &quot;bar&quot;
```
需要解释一下JavaScript的两个东西一个是 `__proto__`,另一个是 `prototype`,这两个东西很容易混淆。这里说明一下:
<li>
**`__proto__`** 主要是安放在一个实际的对象中,用它来产生一个链接,一个原型链,用于寻找方法名或属性,等等。
</li>
<li>
**`prototype`** 是用 `new` 来创建一个对象时构造 `__proto__` 用的。它是构造函数的一个属性。
</li>
在JavaScript中对象有两种表现形式 一种是 `Object` ([ES5关于Object的文档](http://www.ecma-international.org/ecma-262/5.1/#sec-15.2)),一种是 `Function` [ES5关于Function的文档](http://www.ecma-international.org/ecma-262/5.1/#sec-15.2))。
我们可以简单地认为,`__proto__` 是所有对象用于链接原型的一个指针,而 `prototype` 则是 Function 对象的属性,其主要是用来当需要`new`一个对象时让 `__proto__` 指针所指向的地方。 对于超级对象 `Function` 而言, `Function.__proto__` 就是 `Function.prototype`
比如我们有如下的代码:
```
var a = {
x: 10,
calculate: function (z) {
return this.x + this.y + z;
}
};
var b = {
y: 20,
__proto__: a
};
var c = {
y: 30,
__proto__: a
};
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
```
其中的“原型链”如下所示:
<img src="https://static001.geekbang.org/resource/image/f8/7d/f846c45434ca650ab34e518421397d7d.png" alt="" />
注意ES5 中,规定原型继承需要使用 `Object.create()` 函数。如下所示:
```
var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});
```
好了,我们再来看一段代码:
```
// 一种构造函数写法
function Foo(y) {
this.y = y;
}
// 修改 Foo 的 prototype加入一个成员变量 x
Foo.prototype.x = 10;
// 修改 Foo 的 prototype加入一个成员函数 calculate
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;
};
// 现在,我们用 Foo 这个原型来创建 b 和 c
var b = new Foo(20);
var c = new Foo(30);
// 调用原型中的方法,可以得到正确的值
b.calculate(30); // 60
c.calculate(40); // 80
```
那么,在内存中的布局是怎么样的呢?大概是下面这个样子。
<img src="https://static001.geekbang.org/resource/image/e4/80/e4a5053894b27759103976720d29ab80.png" alt="" />
这个图应该可以让你很好地看明白 `__proto__``prototype` 的差别了。
我们可以测试一下:
```
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
b.constructor === Foo, // true
c.constructor === Foo, // true
Foo.prototype.constructor === Foo, // true
b.calculate === b.__proto__.calculate, // true
b.__proto__.calculate === Foo.prototype.calculate // true
```
这里需要说明的是:
**`Foo.prototype` 自动创建了一个属性 `constructor`这是一个指向函数自己的一个reference。这样一来对于实例 `b``c` 来说,就能访问到这个继承的 `constructor` 了。**
有了这些基本概念我们就可以讲一下JavaScript的面向对象编程了。
>
注: 上面示例和图示来源于 [JavaScript, The Core](http://dmitrysoshnikov.com/ecmascript/javascript-the-core/) 一文。
# JavaScript原型编程的面向对象
我们再来重温一下上面讲述的内容:
```
function Person(){}
var p = new Person();
Person.prototype.name = &quot;Hao Chen&quot;;
Person.prototype.sayHello = function(){
console.log(&quot;Hi, I am &quot; + this.name);
}
console.log(p.name); // &quot;Hao Chen&quot;
p.sayHello(); // &quot;Hi, I am Hao Chen&quot;
```
在上面这个例子中:
- 我们先生成了一个空的函数对象 `Person()`
- 然后将这个空的函数对象 `new` 出另一个对象,存在 `p` 中;
- 这时再改变 `Person.prototype`,让其有一个 `name` 的属性和一个 `sayHello()` 的方法;
- 我们发现,另外那个 `p` 的对象也跟着一起改变了。
注意一下:
- 当创建 `function Person(){}` 时,`Person.__proto__` 指向 `Function.prototype`;
- 当创建 `var p = new Person()` 时,`p.__proto__` 指向 `Person.prototype`;
- 当修改了 `Person.prototype` 的内容后,`p.__proto__` 的内容也就被改变了。
好了,我们再来看一下“原型编程”中面向对象的编程玩法。
首先,我们定义一个 `Person` 类。
```
//Define human class
var Person = function (fullName, email) {
this.fullName = fullName;
this.email = email;
this.speak = function(){
console.log(&quot;I speak English!&quot;);
};
this.introduction = function(){
console.log(&quot;Hi, I am &quot; + this.fullName);
};
}
```
上面这个对象中,包含了:
- 属性: `fullName``email`
- 方法: `speak()``introduction()`
其实,所谓的方法也是属性。
然后,我们可以定义一个 `Student` 对象。
```
//Define Student class
var Student = function(fullName, email, school, courses) {
Person.call(this, fullName, email);
// Initialize our Student properties
this.school = school;
this.courses = courses;
// override the &quot;introduction&quot; method
this.introduction= function(){
console.log(&quot;Hi, I am &quot; + this.fullName +
&quot;. I am a student of &quot; + this.school +
&quot;, I study &quot;+ this.courses +&quot;.&quot;);
};
// Add a &quot;exams&quot; method
this.takeExams = function(){
console.log(&quot;This is my exams time!&quot;);
};
};
```
在上面的代码中:
<li>
使用了 `Person.call(this, fullName, email)``call()``apply()` 都是为了动态改变 `this` 所指向的对象的内容而出现的。这里的 `this` 就是 `Student`
</li>
<li>
上面的例子中,我们重载了 `introduction()` 方法,并新增加了一个 `takeExams()`的方法。
</li>
虽然,我们这样定义了 `Student`,但是它还没有和 `Person` 发生继承关系。为了要让它们发生关系,我们就需要修改 `Student` 的原型。
我们可以简单粗暴地做赋值:`Student.__proto__ = Person.prototype` ,但是,这太粗暴了。
我们还是使用比较规范的方式:
<li>
先用 `Object.create()` 来将`Person.prototype``Student.prototype` 关联上。
</li>
<li>
然后,修改一下构造函数 `Student.prototype.constructor = Student;`
</li>
```
// Create a Student.prototype object that inherits
// from Person.prototype.
Student.prototype = Object.create(Person.prototype);
// Set the &quot;constructor&quot; property to refer to Student
Student.prototype.constructor = Student;
```
这样,我们就可以这样使用了。
```
var student = new Student(&quot;Hao Chen&quot;,
&quot;haoel@hotmail.com&quot;,
&quot;XYZ University&quot;,
&quot;Computer Science&quot;);
student.introduction();
student.speak();
student.takeExams();
// Check that instanceof works correctly
console.log(student instanceof Person); // true
console.log(student instanceof Student); // true
```
上述就是基于原型的面向对象编程的玩法了。
>
在ECMAScript标准的第四版开始寻求使JavaScript提供基于类的构造且ECMAScript第六版有提供&quot;class&quot;(类)作为原有的原型架构之上的语法糖,提供构建对象与处理继承时的另一种语法。
# 小结
我们可以看到,这种玩法就是一种委托的方式。在使用委托的基于原型的语言中,运行时语言可以“仅仅通过序列的指针找到匹配”这样的方式来定位属性或者寻找正确的数据。所有这些创建行为、共享的行为需要的是委托指针。
不像是基于类的面向对象语言中类和接口的关系原型和它的分支之间的关系并不要求子对象有相似的内存结构因为如此子对象可以继续修改而无需像基于类的系统那样整理结构。还有一个要提到的地方是不仅仅是数据方法也能被修改。因为这个原因大多数基于原型的语言把数据和方法提作“slots”。
这种在对象里面直接修改的玩法虽然这个特性可以带来运行时的灵活性我们可以在运行时修改一个prototype给它增加甚至删除属性和方法。但是其带来了执行的不确定性也有安全性的问题而代码还变得不可预测这有点黑科技的味道了。因为这些不像静态类型系统没有一个不可变的契约对代码的确定性有保证所以需要使用者来自己保证。
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)

View File

@@ -0,0 +1,383 @@
我们再来看Go语言这个模式Go语言的这个模式挺好玩儿的。声明一个struct跟C很一样然后直接把这个struct类型放到另一个struct里。
# 委托的简单示例
我们来看几个示例:
```
type Widget struct {
X, Y int
}
type Label struct {
Widget // Embedding (delegation)
Text string // Aggregation
X int // Override
}
func (label Label) Paint() {
// [0xc4200141e0] - Label.Paint(&quot;State&quot;)
fmt.Printf(&quot;[%p] - Label.Paint(%q)\n&quot;,
&amp;label, label.Text)
}
```
由上面可知:
<li>
我们声明了一个 `Widget`,其有 `X``Y`
</li>
<li>
然后用它来声明一个 `Label`,直接把 `Widget` 委托进去;
</li>
<li>
然后再给 `Label` 声明并实现了一个 `Paint()` 方法。
</li>
于是,我们就可以这样编程了:
```
label := Label{Widget{10, 10}, &quot;State&quot;, 100}
// X=100, Y=10, Text=State, Widget.X=10
fmt.Printf(&quot;X=%d, Y=%d, Text=%s Widget.X=%d\n&quot;,
label.X, label.Y, label.Text,
label.Widget.X)
fmt.Println()
// {Widget:{X:10 Y:10} Text:State X:100}
// {{10 10} State 100}
fmt.Printf(&quot;%+v\n%v\n&quot;, label, label)
label.Paint()
```
我们可以看到,如果有成员变量重名,则需要手动地解决冲突。
我们继续扩展代码。
先来一个 `Button`
```
type Button struct {
Label // Embedding (delegation)
}
func NewButton(x, y int, text string) Button {
return Button{Label{Widget{x, y}, text, x}}
}
func (button Button) Paint() { // Override
fmt.Printf(&quot;[%p] - Button.Paint(%q)\n&quot;,
&amp;button, button.Text)
}
func (button Button) Click() {
fmt.Printf(&quot;[%p] - Button.Click()\n&quot;, &amp;button)
}
```
再来一个 `ListBox`
```
type ListBox struct {
Widget // Embedding (delegation)
Texts []string // Aggregation
Index int // Aggregation
}
func (listBox ListBox) Paint() {
fmt.Printf(&quot;[%p] - ListBox.Paint(%q)\n&quot;,
&amp;listBox, listBox.Texts)
}
func (listBox ListBox) Click() {
fmt.Printf(&quot;[%p] - ListBox.Click()\n&quot;, &amp;listBox)
}
```
然后,声明两个接口用于多态:
```
type Painter interface {
Paint()
}
type Clicker interface {
Click()
}
```
于是我们就可以这样泛型地使用注意其中的两个for循环
```
button1 := Button{Label{Widget{10, 70}, &quot;OK&quot;, 10}}
button2 := NewButton(50, 70, &quot;Cancel&quot;)
listBox := ListBox{Widget{10, 40},
[]string{&quot;AL&quot;, &quot;AK&quot;, &quot;AZ&quot;, &quot;AR&quot;}, 0}
fmt.Println()
//[0xc4200142d0] - Label.Paint(&quot;State&quot;)
//[0xc420014300] - ListBox.Paint([&quot;AL&quot; &quot;AK&quot; &quot;AZ&quot; &quot;AR&quot;])
//[0xc420014330] - Button.Paint(&quot;OK&quot;)
//[0xc420014360] - Button.Paint(&quot;Cancel&quot;)
for _, painter := range []Painter{label, listBox, button1, button2} {
painter.Paint()
}
fmt.Println()
//[0xc420014450] - ListBox.Click()
//[0xc420014480] - Button.Click()
//[0xc4200144b0] - Button.Click()
for _, widget := range []interface{}{label, listBox, button1, button2} {
if clicker, ok := widget.(Clicker); ok {
clicker.Click()
}
}
```
# 一个 Undo 的委托重构
上面这个是 Go 语中的委托和接口多态的编程方式,其实是面向对象和原型编程综合的玩法。这个玩法可不可以玩得更有意思呢?这是可以的。
首先,我们先声明一个数据容器,其中有 `Add()``Delete()``Contains()` 方法。还有一个转字符串的方法。
```
type IntSet struct {
data map[int]bool
}
func NewIntSet() IntSet {
return IntSet{make(map[int]bool)}
}
func (set *IntSet) Add(x int) {
set.data[x] = true
}
func (set *IntSet) Delete(x int) {
delete(set.data, x)
}
func (set *IntSet) Contains(x int) bool {
return set.data[x]
}
func (set *IntSet) String() string { // Satisfies fmt.Stringer interface
if len(set.data) == 0 {
return &quot;{}&quot;
}
ints := make([]int, 0, len(set.data))
for i := range set.data {
ints = append(ints, i)
}
sort.Ints(ints)
parts := make([]string, 0, len(ints))
for _, i := range ints {
parts = append(parts, fmt.Sprint(i))
}
return &quot;{&quot; + strings.Join(parts, &quot;,&quot;) + &quot;}&quot;
}
```
我们如下使用这个数据容器:
```
ints := NewIntSet()
for _, i := range []int{1, 3, 5, 7} {
ints.Add(i)
fmt.Println(ints)
}
for _, i := range []int{1, 2, 3, 4, 5, 6, 7} {
fmt.Print(i, ints.Contains(i), &quot; &quot;)
ints.Delete(i)
fmt.Println(ints)
}
```
这个数据容器平淡无奇我们想给它加一个Undo的功能。我们可以这样来
```
type UndoableIntSet struct { // Poor style
IntSet // Embedding (delegation)
functions []func()
}
func NewUndoableIntSet() UndoableIntSet {
return UndoableIntSet{NewIntSet(), nil}
}
func (set *UndoableIntSet) Add(x int) { // Override
if !set.Contains(x) {
set.data[x] = true
set.functions = append(set.functions, func() { set.Delete(x) })
} else {
set.functions = append(set.functions, nil)
}
}
func (set *UndoableIntSet) Delete(x int) { // Override
if set.Contains(x) {
delete(set.data, x)
set.functions = append(set.functions, func() { set.Add(x) })
} else {
set.functions = append(set.functions, nil)
}
}
func (set *UndoableIntSet) Undo() error {
if len(set.functions) == 0 {
return errors.New(&quot;No functions to undo&quot;)
}
index := len(set.functions) - 1
if function := set.functions[index]; function != nil {
function()
set.functions[index] = nil // Free closure for garbage collection
}
set.functions = set.functions[:index]
return nil
}
```
于是就可以这样使用了:
```
ints := NewUndoableIntSet()
for _, i := range []int{1, 3, 5, 7} {
ints.Add(i)
fmt.Println(ints)
}
for _, i := range []int{1, 2, 3, 4, 5, 6, 7} {
fmt.Println(i, ints.Contains(i), &quot; &quot;)
ints.Delete(i)
fmt.Println(ints)
}
fmt.Println()
for {
if err := ints.Undo(); err != nil {
break
}
fmt.Println(ints)
}
```
但是,需要注意的是,我们用了一个新的 `UndoableIntSet` 几乎重写了所有的 `IntSet` 和 “写” 相关的方法,这样就可以把操作记录下来,然后 **Undo** 了。
但是可能别的类也需要Undo的功能我是不是要重写所有的需要这个功能的类啊这样的代码类似就是因为数据容器不一样我就要去重写它们这太二了。
我们能不能利用前面学到的泛型编程、函数式编程、IoC等范式来把这个事干得好一些呢当然是可以的。
如下所示:
<li>
我们先声明一个 `Undo[]` 的函数数组(其实是一个栈);
</li>
<li>
并实现一个通用 `Add()`。其需要一个函数指针,并把这个函数指针存放到 `Undo[]` 函数数组中。
</li>
<li>
`Undo()` 的函数中,我们会遍历`Undo[]`函数数组,并执行之,执行完后就弹栈。
</li>
```
type Undo []func()
func (undo *Undo) Add(function func()) {
*undo = append(*undo, function)
}
func (undo *Undo) Undo() error {
functions := *undo
if len(functions) == 0 {
return errors.New(&quot;No functions to undo&quot;)
}
index := len(functions) - 1
if function := functions[index]; function != nil {
function()
functions[index] = nil // Free closure for garbage collection
}
*undo = functions[:index]
return nil
}
```
那么我们的 `IntSet` 就可以改写成如下的形式:
```
type IntSet struct {
data map[int]bool
undo Undo
}
func NewIntSet() IntSet {
return IntSet{data: make(map[int]bool)}
}
```
然后在其中的 `Add``Delete`中实现 Undo 操作。
- `Add` 操作时加入 `Delete` 操作的 Undo。
- `Delete` 操作时加入 `Add` 操作的 Undo。
```
func (set *IntSet) Add(x int) {
if !set.Contains(x) {
set.data[x] = true
set.undo.Add(func() { set.Delete(x) })
} else {
set.undo.Add(nil)
}
}
func (set *IntSet) Delete(x int) {
if set.Contains(x) {
delete(set.data, x)
set.undo.Add(func() { set.Add(x) })
} else {
set.undo.Add(nil)
}
}
func (set *IntSet) Undo() error {
return set.undo.Undo()
}
func (set *IntSet) Contains(x int) bool {
return set.data[x]
}
```
我们再次看到Go语言的Undo接口把Undo的流程给抽象出来而要怎么Undo的事交给了业务代码来维护通过注册一个Undo的方法。这样在Undo的时候就可以回调这个方法来做与业务相关的Undo操作了。
# 小结
这是不是和最一开始的C++的泛型编程很像也和map、reduce、filter这样的只关心控制流程不关心业务逻辑的做法很像而且一开始用一个UndoableIntSet来包装`IntSet`类,到反过来在`IntSet`里依赖`Undo`这就是控制反转IoC。
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)

View File

@@ -0,0 +1,269 @@
前面我们讲了各式各样的不同语言的编程范式从C语言的泛型讲到C++的泛型,再讲到函数式的 Map/Reduce/Filter以及 Pipeline 和 Decorator还有面向对象的多态通过依赖接口而不是实现的桥接模式、策略模式和代理模式以及面向对象的IoC还有JavaScript的原型编程在运行时对对象原型进行修改以及Go语言的委托模式……
所有的这一切,不知道你是否看出一些端倪,或是其中的一些共性来了?
# 两篇论文
1976年瑞士计算机科学家Algol WModulaOberon和Pascal语言的设计师 [Niklaus Emil Wirth](https://en.wikipedia.org/wiki/Niklaus_Wirth)写了一本非常经典的书《[Algorithms + Data Structures = Programs](http://www.ethoberon.ethz.ch/WirthPubl/AD.pdf)》链接为1985年版 ,即算法 + 数据结构 = 程序。
这本书主要写了算法和数据结构的关系,这本书对计算机科学的影响深远,尤其在计算机科学的教育中。
1979年英国逻辑学家和计算机科学家 [Robert Kowalski](https://en.wikipedia.org/wiki/Robert_Kowalski) 发表论文 [Algorithm = Logic + Control](https://www.doc.ic.ac.uk/~rak/papers/algorithm%20=%20logic%20+%20control.pdf),并且主要开发“逻辑编程”相关的工作。
Robert Kowalski是一位逻辑学家和计算机科学家从20世纪70年代末到整个80年代致力于数据库的研究并在用计算机证明数学定理等当年的重要应用上颇有建树尤其是在逻辑、控制和算法等方面提出了革命性的理论极大地影响了数据库、编程语言直至今日的人工智能。
Robert Kowalski在这篇论文里提到
>
An algorithm can be regarded as consisting of a logic component, which specifies the knowledge to be used in solving problems, and a control component, which determines the problem-solving strategies by means of which that knowledge is used. The logic component determines the meaning of the algorithm whereas the control component only affects its efficiency. The efficiency of an algorithm can often be improved by improving the control component without changing the logic of the algorithm. We argue that computer programs would be more often correct and more easily improved and modified if their logic and control aspects were identified and separated in the program text.
翻译过来的意思大概就是:
>
任何算法都会有两个部分, 一个是 Logic 部分这是用来解决实际问题的。另一个是Control部分这是用来决定用什么策略来解决问题。Logic部分是真正意义上的解决问题的算法而Control部分只是影响解决这个问题的效率。程序运行的效率问题和程序的逻辑其实是没有关系的。我们认为如果将 Logic 和 Control 部分有效地分开,那么代码就会变得更容易改进和维护。
注意,最后一句话是重点——**如果将 Logic 和 Control 部分有效地分开,那么代码就会变得更容易改进和维护。**
# 编程的本质
两位老先生的两个表达式:
- Programs = Algorithms + Data Structures
- Algorithm = Logic + Control
第一个表达式倾向于数据结构和算法,它是想把这两个拆分,早期都在走这条路。他们认为,如果数据结构设计得好,算法也会变得简单,而且一个好的通用的算法应该可以用在不同的数据结构上。
第二个表达式则想表达的是数据结构不复杂,复杂的是算法,也就是我们的业务逻辑是复杂的。我们的算法由两个逻辑组成,一个是真正的业务逻辑,另外一种是控制逻辑。程序中有两种代码,一种是真正的业务逻辑代码,另一种代码是控制我们程序的代码,叫控制代码,这根本不是业务逻辑,业务逻辑不关心这个事情。
算法的效率往往可以通过提高控制部分的效率来实现,而无须改变逻辑部分,也就无须改变算法的意义。举个阶乘的例子, X(n)= X(n) * X(n-1) * X(n-2) * X(n-3)** 3 * 2 * 1。逻辑部分用来定义阶乘1 1是0的阶乘 2如果v是x的阶乘且u=v*(x+1)那么u是x+1的阶乘。
用这个定义既可以从上往下地将x+1的阶乘缩小为先计算x的阶乘再将结果乘以1recursive递归也可以由下而上逐个计算一系列阶乘的结果iteration遍历
控制部分用来描述如何使用逻辑。最粗略的看法可以认为“控制”是解决问题的策略,而不会改变算法的意义,因为算法的意义是由逻辑决定的。对同一个逻辑,使用不同控制,所得到的算法,本质是等价的,因为它们解决同样的问题,并得到同样的结果。
因此,我们可以通过逻辑分析,来提高算法的效率,保持它的逻辑,而更好地使用这一逻辑。比如,有时用自上而下的控制替代自下而上,能提高效率。而将自上而下的顺序执行改为并行执行,也会提高效率。
总之,通过这两个表达式,我们可以得出:
**Program = Logic + Control + Data Structure**
前面讲了这么多的编程范式,或是程序设计的方法。其实,我们都是在围绕着这三件事来做的。比如:
<li>
就像函数式编程中的Map/Reduce/Filter它们都是一种控制。而传给这些控制模块的那个Lambda表达式才是我们要解决的问题的逻辑它们共同组成了一个算法。最后我再把数据放在数据结构里进行处理最终就成为了我们的程序。
</li>
<li>
就像我们Go语言的委托模式的那个Undo示例一样。Undo这个事是我们想要解决的问题是Logic但是Undo的流程是控制。
</li>
<li>
就像我们面向对象中依赖于接口而不是实现一样,接口是对逻辑的抽象,真正的逻辑放在不同的具现类中,通过多态或是依赖注入这样的控制来完成对数据在不同情况下的不同处理。
</li>
如果你再仔细地结合我们之前讲的各式各样的编程范式来思考上述这些概念的话,你是否会觉得,所有的语言或编程范式都在解决上面的这些问题。也就是下面的这几个事。
<li>
Control是可以标准化的。比如遍历数据、查找数据、多线程、并发、异步等都是可以标准化的。
</li>
<li>
因为Control需要处理数据所以标准化Control需要标准化Data Structure我们可以通过泛型编程来解决这个事。
</li>
<li>
而Control还要处理用户的业务逻辑即Logic。所以我们可以通过标准化接口/协议来实现我们的Control模式可以适配于任何的Logic。
</li>
上述三点,就是编程范式的本质。
<li>
**有效地分离Logic、Control和Data是写出好程序的关键所在**
</li>
<li>
**有效地分离Logic、Control和Data是写出好程序的关键所在**
</li>
<li>
**有效地分离Logic、Control和Data是写出好程序的关键所在**
</li>
我们在写代码当中,就会看到好多这种代码,会把控制逻辑和业务逻辑放在一块。里面有些变量和流程是跟业务相关的,有些是不相关的。业务逻辑决定了程序的复杂度,业务逻辑本身就复杂,你的代码就不可能写得简单。
Logic它是程序复杂度的的下限然后我们为了控制程序需要再搞出很多控制代码于是Logic+Control的相互交织成为了最终的程序复杂度。
# 把逻辑和控制混淆的示例
我们来看一个示例这是我在leetcode上做的一道题这是通配符匹配给两个字符串匹配。需求如下
```
通配符匹配
isMatch(&quot;aa&quot;,&quot;a&quot;) → false
isMatch(&quot;aa&quot;,&quot;aa&quot;) → true
isMatch(&quot;aaa&quot;,&quot;aa&quot;) → false
isMatch(&quot;aa&quot;, &quot;*&quot;) → true
isMatch(&quot;aa&quot;, &quot;a*&quot;) → true
isMatch(&quot;ab&quot;, &quot;?*&quot;) → true
isMatch(&quot;aab&quot;, &quot;c*a*b&quot;) → false
```
现在你再看看我写出来的代码:
```
bool isMatch(const char *s, const char *p) {
const char *last_s = NULL;
const char *last_p = NULL;
while ( *s != '\0' ) {
if ( *p == '*' ) {
p++;
if ( *p == '\0' ) return true;
last_s = s;
last_p = p;
} else if ( *p == '?' || *s == *p ) {
s++;
p++;
} else if ( last_s != NULL {
p = last_p;
s = ++last_s;
} else {
return false;
}
}
while ( *p == '*' ) p++;
return *p == '\0';
}
```
我也不知道我怎么写出来的,好像是为了要通过,我需要关注于性能,你看,上面这段代码有多乱。如果我不写注释你可能都看不懂了。就算我写了注释以后,你敢改吗?你可能连动都不敢动(哈哈)。上面这些代码里面很多都不是业务逻辑,是用来控制程序的逻辑。
业务逻辑是相对复杂的,但是控制逻辑跟业务逻辑交叉在一块,虽然代码写得不多,但是这个代码已经够复杂了。两三天以后,我回头看,我到底写的什么,我也不懂,为什么会写成这样?我当时脑子是怎么想的?我完全不知道。我现在就是这种感觉。
那么,怎么把上面那段代码写得更好一些呢?
<li>
首先我们需要一个比较通用的状态机NFA非确定有限自动机或者DFA确定性有限自动机来维护匹配的开始和结束的状态。这属于Control。
</li>
<li>
如果我们做得好的话还可以抽象出一个像程序的文法分析一样的东西。这也是Control。
</li>
<li>
然后,我们把匹配 `*``?` 的算法形成不同的匹配策略。
</li>
这样,我们的代码就会变得漂亮一些了,而且也会快速一些。
这里有篇正则表达式的高效算法的论文[Regular Expression Matching Can Be Simple And Fast](https://swtch.com/~rsc/regexp/regexp1.html),推荐你读一读,里面有相关的实现,我在这里就不多说了。
这里想说的程序的本质是Logic+Control+Data而其中Logic和Control是关键。注意这个和系统架构也有相通的地方逻辑是你的业务逻辑逻辑过程的抽象加上一个由术语表示的数据结构的定义控制逻辑跟你的业务逻辑是没关系的你控制它执行。
控制一个程序流转的方式,即程序执行的方式,并行还是串行,同步还是异步,以及调度不同执行路径或模块,数据之间的存储关系,这些和业务逻辑没有关系。
<img src="https://static001.geekbang.org/resource/image/4a/92/4a8c7c77df1f1a6b3ff701577986ee92.png" alt="" />
如果你看过那些混乱不堪的代码你会发现其中最大的问题是我们把这Logic和Control纠缠在一起了所以会导致代码很混乱难以维护Bug很多。绝大多数程序复杂的原因就是这个问题就如同下面这幅图中表现的情况一样。
<img src="https://static001.geekbang.org/resource/image/5f/e2/5f45a22a027375c5960f5a6b31159ce2.png" alt="" />
# 再来一个简单的示例
这里给一个简单的示例。
下面是一段检查用户表单信息的常见代码,我相信这样的代码你见得多了。
```
function check_form_x() {
var name = $('#name').val();
if (null == name || name.length &lt;= 3) {
return { status : 1, message: 'Invalid name' };
}
var password = $('#password').val();
if (null == password || password.length &lt;= 8) {
return { status : 2, message: 'Invalid password' };
}
var repeat_password = $('#repeat_password').val();
if (repeat_password != password.length) {
return { status : 3, message: 'Password and repeat password mismatch' };
}
var email = $('#email').val();
if (check_email_format(email)) {
return { status : 4, message: 'Invalid email' };
}
...
return { status : 0, message: 'OK' };
}
```
但其实我们可以做一个DSL+一个DSL的解析器比如
```
var meta_create_user = {
form_id : 'create_user',
fields : [
{ id : 'name', type : 'text', min_length : 3 },
{ id : 'password', type : 'password', min_length : 8 },
{ id : 'repeat-password', type : 'password', min_length : 8 },
{ id : 'email', type : 'email' }
]
};
var r = check_form(meta_create_user);
```
这样DSL的描述是“Logic”而我们的 `check_form` 则成了“Control”代码就非常好看了。
# 小结
代码复杂度的原因:
- 业务逻辑的复杂度决定了代码的复杂度;
- 控制逻辑的复杂度 + 业务逻辑的复杂度 ==&gt; 程序代码的混乱不堪;
- 绝大多数程序复杂混乱的根本原因:**业务逻辑与控制逻辑的耦合**。
如何分离control和logic呢我们可以使用下面的这些技术来解耦。
<li>
**State Machine**
<ul>
- 状态定义
- 状态变迁条件
- 状态的action
**DSL Domain Specific Language**
- HTMLSQLUnix Shell ScriptAWK正则表达式……
**编程范式**
- 面向对象委托、策略、桥接、修饰、IoC/DIP、MVC……
- 函数式编程:修饰、管道、拼装
- 逻辑推导式编程Prolog
**这就是编程的本质:**
- **Logic部分才是真正有意义的What**
- **Control部分只是影响Logic部分的效率How**
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)

View File

@@ -0,0 +1,134 @@
这篇文章重点介绍Prolog语言。PrologProgramming in Logic是一种逻辑编程语言它创建在逻辑学的理论基础之上最初被运用于自然语言等研究领域。现在它已被广泛地应用在人工智能的研究中可以用来建造专家系统、自然语言理解、智能知识库等。
Prolog语言最早由艾克斯马赛大学Aix-Marseille University的Alain Colmerauer与Philippe Roussel等人于20年代60年代末研究开发的。1972年被公认为是Prolog语言正式诞生的年份自1972年以后分支出多种Prolog的方言。
最主要的两种方言为Edinburgh和Aix-Marseille。最早的Prolog解释器由Roussel建造而第一个Prolog编译器则是David Warren编写的。
Prolog一直在北美和欧洲被广泛使用。日本政府曾经为了建造智能计算机而用Prolog来开发ICOT第五代计算机系统。在早期的机器智能研究领域Prolog曾经是主要的开发工具。
20世纪80年代Borland开发的Turbo Prolog进一步普及了Prolog的使用。1995年确定了ISO Prolog标准。
有别于一般的函数式语言Prolog的程序是基于谓词逻辑的理论。最基本的写法是定立对象与对象之间的关系之后可以用询问目标的方式来查询各种对象之间的关系。系统会自动进行匹配及回溯找出所询问的答案。
Prolog代码中以大写字母开头的元素是变量字符串、数字或以小写字母开头的元素是常量下划线_被称为匿名变量。
# Prolog的语言特征
逻辑编程是靠推理,比如下面的示例:
```
program mortal(X) :- philosopher(X).
philosopher(Socrates).
philosopher(Plato).
philosopher(Aristotle).
mortal_report:-
write('Known mortals are:'), nl, mortal(X),
write(X),nl,
fail.
```
我们可以看到下面的几个步骤。
1. 先定义一个规则:哲学家是人类。
1. 然后陈述事实:苏格拉底、亚里士多德、柏拉图都是哲学家。
1. 然后,我们问,谁是人类?于是就会输出苏格拉底、亚里士多德、柏拉图。
下面是逻辑编程范式的几个特征。
- 逻辑编程的要点是将正规的逻辑风格带入计算机程序设计之中。
- 逻辑编程建立了描述一个问题里的世界的逻辑模型。
- 逻辑编程的目标是对它的模型建立新的陈述。
- 通过陈述事实——因果关系。
- 程序自动推导出相关的逻辑。
# 经典问题:地图着色问题
我们再来看一个经典的四色地图问题。任何一个地图,相邻区域不能用相同颜色,只要用四种不同的颜色就够了。
<img src="https://static001.geekbang.org/resource/image/db/cb/db670cfbe7497d71eba70d60d8aa0fcb.png" alt="" />
首先,定义四种颜色。
```
color(red).
color(green).
color(blue).
color(yellow).
```
然后,定义一个规则:相邻的两个地区不能用相同的颜色。
```
neighbor(StateAColor, StateBColor) :- color(StateAColor), color(StateBColor),
StateAColor \= StateBColor. /* \= is the not equal operator */
```
最前面的两个条件:`color(StateAColor)``color(StateBColor)` 表明了两个变量 `StateAColor``StateBColor`。然后,第三个条件: `StateAColor \= StateBColor` 表示颜色不能相同。
接下来的事就比较简单了。我们描述事实就好了,描述哪些区域是相邻的事实。
比如,下面描述了 BW 和 BY 是相邻的。
`germany(BW, BY) :- neighbor(BW, BY).`
下面则描述多个区 BW、 BY、 SL、 RP、 和 ND 的相邻关系:
`germany(BW, BY, SL, RP, HE) :- neighbor(BW, BY), neighbor(BW, RP), neighbor(BW, HE).`
于是,我们就可以描述整个德国地图的相邻关系了。
```
germany(SH, MV, HH, HB, NI, ST, BE, BB, SN, NW, HE, TH, RP, SL, BW, BY) :-
neighbor(SH, NI), neighbor(SH, HH), neighbor(SH, MV),
neighbor(HH, NI),
neighbor(MV, NI), neighbor(MV, BB),
neighbor(NI, HB), neighbor(NI, BB), neighbor(NI, ST), neighbor(NI, TH),
neighbor(NI, HE), neighbor(NI, NW),
neighbor(ST, BB), neighbor(ST, SN), neighbor(ST, TH),
neighbor(BB, BE), neighbor(BB, SN),
neighbor(NW, HE), neighbor(NW, RP),
neighbor(SN, TH), neighbor(SN, BY),
neighbor(RP, SL), neighbor(RP, HE), neighbor(RP, BW),
neighbor(HE, BW), neighbor(HE, TH), neighbor(HE, BY),
neighbor(TH, BY),
neighbor(BW, BY).
```
最后我们使用如下语句就可以让Prolog推导到各个地区的颜色。
```
?- germany(SH, MV, HH, HB, NI, ST, BE, BB, SN, NW, HE, TH, RP, SL, BW, BY).
```
# 小结
Prolog这种逻辑编程把业务逻辑或是说算法抽象成只关心规则、事实和问题的推导这样的标准方式不需要关心程序控制也不需要关心具体的实现算法。只需要给出可以用于推导的规则和相关的事实问题就可以被通过逻辑推导来解决掉。是不是很有意思也很好玩
如果有兴趣,你可以学习一下,这里推荐两个学习资源:
- [Prolog Tutorial](http://www.doc.gold.ac.uk/~mas02gw/prolog_tutorial/prologpages/)
- [Learn Prolog Now!](http://www.learnprolognow.org)
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)

View File

@@ -0,0 +1,67 @@
这个世界到今天已经有很多很多的编程范式,相当复杂。下面这个图比较好地描绘了这些各式各样的编程范式,这个图越往左边就越是“声明式的”,越往右边就越不是“声明式的”(指令式的),我们可以看到,函数式编程和逻辑编程,都在左边,而右边是指令式的,有状态的,有类型的。
<img src="https://static001.geekbang.org/resource/image/9d/8d/9d6ac4820cc070a6b567d3f514d9ea8d.png" alt="" />
上面这个图有点乱,不过总体说来,我们可以简单地把这世界上纷乱的编程范式,分成这几类:**声明式**、**命令式**、**逻辑的**、**函数式**、**面向对象的**、**面向过程的**。
于是我们归纳一下,就可以得到下面这个简单的图。简单描述一下:
- 中间两个声明式编程范式(函数式和逻辑式)偏向于你定义要什么,而不是怎么做。
- 而两边的命令式编程范式和面向对象编程范式,偏向于怎么做,而不是要做什么。
<img src="https://static001.geekbang.org/resource/image/d6/50/d64bf8275ee9e0eac3112dcd342d9350.png" alt="" />
我们再归纳一下,基本上来说,就是两大分支,一边是在解决数据和算法,一边是在解决逻辑和控制。
<img src="https://static001.geekbang.org/resource/image/bf/ef/bf6945c2ca2ec5564ecbbf1c81503eef.png" alt="" />
下面再结合一张表格说明一下这世界上四大编程范式的类别,以及它们的特性和主要的编程语言。
<img src="https://static001.geekbang.org/resource/image/fc/ab/fcd2780bcb35c17e475eedb94b1f66ab.png" alt="" />
程序编程范式。一个是左脑一个右脑。我们程序员基本上是在用左脑左脑是理性分析喜欢数据证据线性思维陷入细节具体化的不抽象。但是实际上玩儿出这些东西的都在右脑函数式还有像逻辑式的抽象能力都在右脑。所以我们非线性的想象力都在这边而标准化教育把我们这边已经全部干掉了我们只剩左边。我们陷入细节我一说Java是最好的程序设计语言一堆人就来了找各种各样的细节问题跟你纠缠。
离我们最近的是函数式编程,但既然函数式编程这么好,为什么函数式编程火不起来呢?首先,这里有个逻辑上的问题,并不是用的人越多的东西就越好。因为还要看是不是大多数人都能理解的东西。函数式编程或是声明式编程,需要的是用我们的右脑,而指令式的则需要用我们的左脑。
参看下图:
<img src="https://static001.geekbang.org/resource/image/11/c7/11f63d119d5954724b42024f9d6a64c7.png" alt="" />
我们可以看到,
**人的左脑的特性是**
- 理性分析型
- 喜欢数据证据
- 线性思维
- 陷入细节
- 具体化的
**人的右脑的特性是**
- 直觉型
- 想象力
- 非线性
- 宏观思维
- 抽象化的
人类社会中,绝大多数人都是左脑型的人,而只有少数人是右脑型的人,比如那些哲学家、艺术家,以及能够创造理论知识的人。这些人在这个世界上太少了。
这是为什么很多人理解和使用声明式的编程范式比较有困难,因为这要用你的右脑,但是我们习惯于用我们的左脑,左脑用多了以后右脑就有点跟不上了。
说到人类的大脑了,已经到了不是我专长的地方了,这个话题太大了,所以,也是时候结束《编程范式游记》这一系列文章了。希望你能从这一系列文章中有所收获。如果有什么疑问或是我有什么没有讲对的,还希望得到你的批评和指正。先谢谢了。
以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。**这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。**
- [01 | 编程范式游记:起源](https://time.geekbang.org/column/article/301)
- [02 | 编程范式游记:泛型编程](https://time.geekbang.org/column/article/303)
- [03 | 编程范式游记:类型系统和泛型的本质](https://time.geekbang.org/column/article/2017)
- [04 | 编程范式游记:函数式编程](https://time.geekbang.org/column/article/2711)
- [05 | 编程范式游记:修饰器模式](https://time.geekbang.org/column/article/2723)
- [06 | 编程范式游记:面向对象编程](https://time.geekbang.org/column/article/2729)
- [07 | 编程范式游记:基于原型的编程范式](https://time.geekbang.org/column/article/2741)
- [08 | 编程范式游记Go 语言的委托模式](https://time.geekbang.org/column/article/2748)
- [09 | 编程范式游记:编程的本质](https://time.geekbang.org/column/article/2751)
- [10 | 编程范式游记:逻辑编程范式](https://time.geekbang.org/column/article/2752)
- [11 | 编程范式游记:程序世界里的编程范式](https://time.geekbang.org/column/article/2754)