编程的本质
两篇论文
1976 年,瑞士计算机科学家,Algol W,Modula,Oberon 和 Pascal 语言的设计师 Niklaus Emil Wirth写了一本非常经典的书《Algorithms + Data Structures = Programs》(链接为 1985 年版) ,即算法 + 数据结构 = 程序。
这本书主要写了算法和数据结构的关系,这本书对计算机科学的影响深远,尤其在计算机科学的教育中。1979 年,英国逻辑学家和计算机科学家 Robert Kowalski 发表论文 Algorithm = Logic + Control,并且主要开发“逻辑编程”相关的工作。
Robert Kowalski 是一位逻辑学家和计算机科学家,从 20 世纪 70 年代末到整个 80 年代致力于数据库的研究,并在用计算机证明数学定理等当年的重要应用上颇有建树,尤其是在逻辑、控制和算法等方面提出了革命性的理论,极大地影响了数据库、编程语言,直至今日的人工智能。
Robert Kowalski 在这篇论文里提到:
任何算法都会有两个部分, 一个是 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 是 0 的阶乘;
-
如果 v 是 x 的阶乘,且 u=v*(x+1),那么 u 是 x+1 的阶乘
用这个定义,既可以从上往下地将 x+1 的阶乘缩小为先计算 x 的阶乘,再将结果乘以 1(recursive,递归),也可以由下而上逐个计算一系列阶乘的结果(iteration,遍历)。
控制部分用来描述如何使用逻辑。最粗略的看法可以认为“控制”是解决问题的策略,而不会改变算法的意义,因为算法的意义是由逻辑决定的。
对同一个逻辑,使用不同控制,所得到的算法,本质是等价的,因为它们解决同样的问题,并得到同样的结果。
因此,我们可以通过逻辑分析,来提高算法的效率,保持它的逻辑,而更好地使用这一逻辑。
比如,有时用自上而下的控制替代自下而上,能提高效率。
而将自上而下的顺序执行改为并行执行,也会提高效率。
总之,通过这两个表达式,我们可以得出:
Program = Logic + Control + Data Structure
基本上所有的语言或者编程范式都是在解决上面的问题。也就是下面的这几个事:
- Control 是可以标准化的。比如:遍历数据、查找数据、多线程、并发、异步等,都是可以标准化的
- 因为 Control 需要处理数据,所以标准化 Control,需要标准化 Data Structure,我们可以通过泛型编程来解决这个事
- 而 Control 还要处理用户的业务逻辑,即 Logic。所以,我们可以通过标准化接口 / 协议来实现,我们的 Control 模式可以适配于任何的 Logic
上述三点,就是编程范式的本质。
-
有效地分离 Logic、Control 和 Data 是写出好程序的关键所在!
-
有效地分离 Logic、Control 和 Data 是写出好程序的关键所在!
-
有效地分离 Logic、Control 和 Data 是写出好程序的关键所在!
我们在写代码当中,就会看到好多这种代码,会把控制逻辑和业务逻辑放在一块。
里面有些变量和流程是跟业务相关的,有些是不相关的。
业务逻辑决定了程序的复杂度,业务逻辑本身就复杂,你的代码就不可能写得简单。
Logic,它是程序复杂度的的下限,然后,我们为了控制程序,需要再搞出很多控制代码,于是 Logic+Control 的相互交织成为了最终的程序复杂度。
把逻辑和控制混淆的示例
以冒泡排序为例子。
func sortMaoPao(nums []int) []int {
var temp int
for i := 0; i < len(nums); i++ {
for j := i + 1; j < len(nums); j++ {
if nums[i] > nums[j] {
//nums[i], nums[j] = nums[j], nums[i]
temp = nums[i]
nums[i] = nums[j]
nums[j] = temp
}
}
}
return nums
}
排序的逻辑无非就是遍历,比较大小,交换。
那我们可以把 logic 抽象出来。便于其他的排序复用代码,也更利于理解程序!
如对很多其他的数据结构如结构体中的数据的排序,代码可复用,这就是接口方法的好处了。
即写三个接口方法:
- Len() int 返回长度
- Less(i, j int) bool 下边为 i 的数据是否小于 下标为 j 的数据
- Swap(i, j int) 交换
package main
import "fmt"
type sorter interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
type data struct {
nums []int
}
func main() {
d := &data{[]int{1, 3, 2, 4, 6}}
sortMaoPao(d)
fmt.Println(*d) //[1, 2, 3, 4, 6]
}
func (d data) Len() int {
return len(d.nums)
}
func (d data) Less(i, j int) bool {
if d.nums[i] > d.nums[j] {
return true
}
return false
}
func (d *data) Swap(i, j int) {
var temp int
temp = d.nums[i]
d.nums[i] = d.nums[j]
d.nums[j] = temp
}
func sortMaoPao(s sorter) {
length := s.Len()
for i := 0; i < length; i++ {
for j := i + 1; j < length; j++ {
if s.Less(i, j) {
s.Swap(i, j)
}
}
}
}
尽管代码变多了,但是我们完全将逻辑与控制分开了。
Len() 只求大小,Less()只比较大小,Swap()只用来交换。
另外写了接口用法用于代码复用。
我们要追求高质量代码,而不仅仅是写出来!
推荐
我总结了一些编程中常用的书籍以及大佬20年的编程经验。
对于学习非常的有帮助!
公众号回复:书籍 领取