Contents

Programming Paradigm Thoughts

编程范式 Programming Paradigm

来源于左耳朵耗子文章总结,记录一些自己的理解和文章的观点。不记录具体示例。

泛型编程

通常说 程序=算法+数据结构。而算法的通用性需要做到:

  1. 一个通用的算法,需要对所处理的数据 的数据类型 进行适配。
  2. 算法其实是在操作数据结构,而数据是放到数据结构中的。所以,真正的泛型除了适配数据类型外,还要适配数据结构。而适配数据结构很复杂,比如容器内存的分配和释放,不同的数据结构可能有不一样的内存分配和释放模型。这就导致了泛型算法的复杂度急剧上升。
  3. 最后,在实现泛型算法的时候,你会发现自己在纠结哪些东西 应该抛给调用者处理,哪些又是可以封装起来,如何平衡和选择 并没有定论,也不好解决。

理想情况下,算法应该和数据结构以及类型是无关的。数据结构只要做好存储数据,算法只关心一个标准的实现。所以,为了实现算法的通用性,我们要求 将抽象的数据类型 传递给算法,这也就产生了 泛型抽象。

C++通过类的方式、通过模板达到类型和算法的妥协、通过虚函数和运行时类型识别。

不论什么语言,形式是多样的,但是原理是相同的。通过泛型来提示算法的通用性。

泛型的本质

要了解泛型的本质,就要先了解 类型系统。

类型系统

计算机中,类型系统 用于定义如何将 编程语言中数值和表达式 归类为许多不同的类型,以及如何操作这些类型,还有这些类型如何相互作用。类型可以确认一个值或者一组值具有特定的意义和目的。

一般来说,编程语言者会有两种类型,一种是内建类型,如 int、float 和 char 等,一种是抽象类型,如 struct、class 和 function 等。抽象类型在程序运行中,可能不表示为值。类型系统在各种语言之间有非常大的不同,也许,最主要的差异存在于编译时期的语法,以及运行时期的操作实现方式。

类型系统提供如下功能:程序语言的安全性、利于编译器的优化、代码的可读性、抽象化。

类型系统 带来的问题

对应不同的类型,需要写出不同的处理逻辑,如果要做到泛型,就要从底层操作。

动态语言 也是有类型系统

动态语言 变量的类型 是由运行时的解释器 来动态标记的。所以 无论哪种程序语言,都逃避免不了一个特定的类型系统。哪怕是可随意改变变量类型的动态类型的语言,我们在读代码的过程中也需要脑补某个变量在运行时的类型。

静态语言和动态语言 类型检查

  • 静态类型检查 是在编译器进行语义分析时进行的。如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换),那么称此处理为强类型,反之称为弱类型。

  • 动态类型检查 系统更多的是在运行时期做动态类型标记和相关检查。所以,动态类型的语言必然要给出一堆诸如:is_array(), is_int(), is_string() 或是 typeof() 这样的运行时类型检查函数。

动态语言代码量比较大后,类型问题可能会引起严重问题。静态语言的支持者会说编译器会帮我们找到这些问题,而动态语言的支持者则认为,静态语言的编译器也无法找到所有的问题,**想真正提前找到问题只能通过测试来解决。**这里,我很赞同。语言本身不是主要问题,通过外部的详细测试来保证 逻辑的可靠性。

类型的本质

  • 类型是对内存的一种抽象。不同的类型,会有不同的内存布局和内存分配策略。

  • 不同的类型,有不同的操作。所以,对于特定的类型,也有特定的一组操作。

所以,要做到泛型,我们需要做到下面的事情:

标准化掉类型的内存分配、释放和访问。标准化掉类型的操作(比较/io/复制操作等)。标准化掉数据容器的操作(查找/过滤/聚合等)。

所以C++做了很多复杂的技术来达到泛型编程的目标。

泛型编程定义

Generic Programming Definition

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

屏蔽掉数据和操作数据的细节,让算法更为通用,让编程者更多地关注算法的结构,而不是在算法中处理不同的数据类型。

函数式编程

为了更为抽象的泛型

它的理念就是借用于数学中的代数,通过组合一个个函数,来实现一组操作。

对于函数式编程来说,其只关心,定义输入数据和输出数据相关的关系,数学表达式里面其实是在做一种映射(mapping),输入的数据和输出的数据关系是什么样的,是用函数来定义的。

1、fp的特点:

  • stateless:函数不维护任何状态。即函数内部不能存在状态。无状态对并行执行无伤害。
  • immutable:输入数据不能改变,要返回新的数据集。

2、fp的技术点:

  • first class function: 函数是一等公民。把函数当成变量一样,创建传递调用。

  • tail recursion optimization: 尾递归优化。过深的递归,会让stack受不了。因此,我们可以通过尾递归优化,重用stack 提升性能。当然这需要语言支持。

  • map & reduce: 对集合的操作,代码更容易阅读。 如lodash中map/reduce。

  • pipeline: 函数管道。将函数实例放在list中,然后遍历这个list,对输入的数据 依次用list里的函数操作,最终得到我们的结果。

  • recursing: 递归。递归可以把一个复杂的问题 用简单的代码描述出来。这是FP的精髓。

  • currying: 柯里化。将一个函数的多个参数分解成多个函数,然后将函数多层封装起来,每层函数都返回一个函数去接收下一个参数,这可以简化函数的多个参数。参考js-curry

  • higher order function: 高阶函数。高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出,就像面向对象对象满天飞一样。这个技术用来做 Decorator 很不错。可以类比到React里的HOC。

fp的思维方式

函数式编程关注的是:describe what to do, rather than how to do it。于是,我们把以前的过程式编程范式叫做 Imperative Programming – 指令式编程,而把函数式编程范式叫做 Declarative Programming – 声明式编程。

所以,我们在实现一段逻辑代码时。将其分成 若干个 小函数,**注意这些小函数间 不能有共享变量。**这样才能达到函数的无状态。然后将 这些小函数 拼装起来。这些小函数的特点:无共享变量,函数间通过参数和返回值来传递数据,函数里没有临时变量。

面向对象编程

现实中业务代码,肯定会有状态和数据处理。OOP就可以很好的处理这些问题。

OOP是具有对象概念的编程范型。一个类可以包含 数据、属性、代码和方法。对象就是类的实例。OOP将对象作为 程序的基本单元。将程序和数据封装其中,以提高软件的复用性

OOP可以看做 程序中各种独立的对象 相互调用。这比传统的程序设计(操作函数或执行指令),更具有灵活性和可维护性。

核心理念

  • “Program to an ‘interface’, not an ‘implementation’.”

    • 使用者不需要知道 数据类型、结构、算法的细节。
    • 使用者不需要知道 实现细节,只需要知道 提供的接口。
    • 利于抽象、封装、动态绑定、多态。
  • “Favor ‘object composition’ over ‘class inheritance’.”

    • 继承需要给子类暴露一些父类的设计和实现细节。
    • 父类实现的改变会造成子类也需要改变。
    • 我们以为继承主要是为了代码重用,但实际上在子类中需要重新实现很多父类的方法。
    • 继承更多的应该是为了多态。

上面说的是,接口的作用和组合优于继承。这些在之前的SOLIDEffectiveJava中都有体现。

当然,OOP也有不太好的地方。通过对象来达到抽象结果,把代码分散在不同的类里面,然后,要让它们执行起来,就需要把这些类粘合起来。所以,它另外一方面鼓励相当厚重的代码黏合层(代码黏合层就是把代码黏合到这里面)。像Java里有很多注入方式,如spring。导致大量的封装,屏蔽了内部细节。具体发生什么事还是不知道。

编程本质

  • Programs = Algorithms + Data Structures
  • Algorithm = Logic + Control

第一个表达式倾向于数据结构和算法。认为如果数据结构设计的好,算法也会变得简单。而且一个好的通用的算法应该可以用在不同的数据结构上。

第二个表达式:复杂的是算法。我们的算法由,业务逻辑和控制逻辑 组成。而算法的效率可以通过 控制部分的效率来提高。

通过上面总结得出:

Program = Logic + Control + Data Structure

前面说的编程范式设计方法,都是在围绕这三个部分来解决。总体逻辑是:

  1. Control是可以标准化的。如:遍历、查找、并发、异步等,都是可以标准化的。
  2. 因为Control需要处理数据,所以标准化Control后,需要标准化DataStructure,所以我们用泛型来解决,数据结构的标准化。
  3. 因为Control还有处理业务逻辑,即Logic。所以,我们通过标准化接口/协议来实现,让Control可以适配任何Logic,类比一下设计模式。

上面3点就是编程范式本质,通过Control来组合Logic和Data。有效的分离出 Logic、Control、Data是写出好程序的关键。 而对Logic、Control、Data的标准化处理,能够让程序更加灵活与通用。所以,程序的灵活就要: L/C/D 分离和标准化

示例:我们平时会看到,有些代码将 控制逻辑 和 业务逻辑 放在一块。里面有些 变量和流程 有和业务相关,也有无关的。如果程序Logic本身就很复杂,我们又把Control和其搅在一起,就造成程序最终很复杂。

而如何分离Control和Logic呢,通过以下技术来解耦:

  • State Machine
    • 状态定义
    • 状态变迁条件
    • 状态的action
  • DSL - Domain Specific Language
    • HTML SQL 正则表达式…
  • 编程范式
    • OOP:委托、策略、桥接、修饰、IOC/DIP、MVC…
    • FP:修饰、管道、拼装

小结:

编程的本质在于Logic和Control。Logic告诉我们做什么(what),Control影响我们怎么去做(How)。

而如何做好,就要考虑 分离 和 标准化。分离是为了解耦,让代码清晰。标准化是为了灵活,通用泛型/设计模式/通用操作等,让代码适配更多情况。

分离体现:

  • OOP: SOLID
  • FP: 函数柯里化
  • 其他:DSL、状态机

标准化体现:泛型、接口/协议

reference