编程范式 | 青训营笔记

107 阅读7分钟

课程介绍

课程背景

  1. 前端的主要编程语言为 JavaScript
  2. JavaScript 作为一种融合了多种编程范式的语言,灵活性非常高
  3. 前端开发人员需要根据场景在不同编程范式之间 自如切换
  4. 进一步需要创造领域特定语言抽象业务问题

课程收益

  1. 了解不同编程范式的起源和适用场景
  2. 掌握 JavaScript 在不同的编程范式,特别是函数式编程范式的使用
  3. 掌握创建领域特定语言的相关工具和模式

编程语言

01_为什么需要编程语言

程序员需要通过指令输入告诉计算机需要做什么,这种指令被称为 编程语言

02_编程语言的发展史

  • 最开始的时候,人们是通过 机器语言 来和机器进行交流。这种方式对机器友好,但是对人类来说十分不友好。程序员最开始的时候需要 在纸带上打孔 才能和机器进行交流。之后虽然出现了 线缆/开关 代替纸带,但是当需要输入大量内容的时候仍然很繁琐
  • 之后出现了 汇编语言 代替机器语言。汇编语言是使用字母来代替数字来进行输入。在不同的机器架构下面,汇编语言可以转换成不同的数字。极大方便了代码的输入。
  • 之后,为了更加方便快捷的输入,创建了 高级语言。 相较于汇编语言,高级语言更加接近于自然语言,输入也更为快捷。编辑器随之诞生。编辑器可以将 高级语言 转换成 汇编语言,然后将 汇编语言 转换成 机器语言 让计算机识读。

03_高级语言

  • C:“中级语言” 过程式语言 代表
    • 可对字节地址 直接操作
    • 代码和数据分离,倡导结构化编程
    • 功能齐全:数据类型和控制逻辑多样化
    • 可移植能力强
  • C++:面向对象语言 代表
    • C with Classes
    • 继承
    • 权限控制
    • 虚函数
    • 多态
  • Lisp:函数式语言 代表
    • 与机器无关
    • 列表:代码即数据
    • 闭包
  • JavaScript:基于 原型头等函数多范式语言
    • 过程式
    • 面向对象
    • 函数式
    • 响应式

总结

编程语言机器语言
汇编语言
中级语言面向过程代表C
高级语言面向对象代表C++
函数式代表lisp
多范式代表javascript

编程范式

01_什么是编程范式

程序语言特性:

  • 是否允许副作用
  • 操作的执行顺序
  • 代码组织
  • 状态管理
  • 语法和词法

02_常见编程范式

编程范式

  • 命令式 —— 程序员如何操作机器改变状态
    • 面向过程 —— 操作用 过程 进行分组
    • 面向对象 —— 根据 操作和它对应的状态 进行分组
  • 声明式 —— 程序员声明响应的结果而不指明具体的操作
    • 函数式 —— 通过 一系列的函数 来声明逻辑
    • 响应式 —— 通过 数据流映射函数 来表示结果

03_过程式编程

  • 自顶向下
    • 设计程序的时候要自顶向下来设计程序。
      • 程序
        • 模块
          • 变量
            • 数据结构
          • 函数
            • 函数
            • 语句
  • 结构化编程
    • 顺序结构
    • 选择结构 if
    • 循环结构 while

JS 中的面向过程

// 可以看作 过程式编程 中的数据
export var car = {
  meter:100,
  speed:10
};

// 可以看作 过程式编程 中的算法
export function advanceCar(meter){
  while (car < meter) {
    car.meter += car.speed
  }
}
import { car advanceCar } from './car';
function main(){
  console.log('before',car);
  
  advanceCar(1000);
  
  console.log('after',car)
}

面向过程式编程的缺点

  • 数据与算法关系弱

    随着程序规模的增大,函数和数据之间的关系会变得难以理解,很难看出来函数的调用关系,很难看出某个数据可以用哪些函数进行修改,某个函数可以修改哪些数据。当数据的值出错的时候很难看出是哪个函数导致的,显得程序的查错会非常困难

  • 不利于修改和扩充

    • 当变量的定义有所改动的时候,需要把所用使用该变量的语句都找出来进行修改。
  • 不利于代码重用

    • 随着程序的增大,大量函数直接的关系错综复杂,在功能类似的其他地方很难复用

04_面向对象编程

将数据封装到 类 的 实例 当中,把数据藏起来,让外部不能直接操纵,只能通过类当中的实例方法来获取和修改数据,限制了数据的访问方式

  • 封装
  • 继承
  • 多态
  • 依赖注入*

封装

将一些 客观的事物 封装成 具体的类;类可以把数据和方法只让可信的类或者对象进行操作,对不可信的进行隐藏。一个类就是 封装了 数据 和 操作这些数据的代码 的逻辑形式

  • 关联数据与算法
class Car {
  // 数据
  meter = 100;
  speed = 100;
  
  
  // 算法
  advance(meter){
    while(this.meter < meter){
      this.meter += this.speed;
    }
  }
  
  getSpec(){
    return `meter:${this.meter};speed:${this.speed};`
  }
  
}
function main(){
  
  var car = new Car();
  car.advance(1000);

}

继承

可以让某个类型的对象可以获得另一个类型的对象的属性和方法,可以使用现有类的所有功能,并且在无需编写原有类的情况下对这些类进行扩充

  • 无需重写的情况下进行功能扩充
class FlyCar extends Car {
  
  // 新数据
  height = 100
  
  // 新算法
  fly(height){
    while (this.height < height){
      this.height += this.speed
    }
  }
}

多态

相同的方法在不同的场景有不同的表现形式

  • 不同的结构可以进行接口共享,进而达到函数复用
class FlyCar extends Car {
  
  height = 100
  
  fly(height){
    while (this.height < height){
      this.height += this.speed
    }
  }
  
  // 接口复用
  getSpec() {
    return super.getSpec() +  `height:${this.height};`
  }
}


// 函数复用
function showCarSpec(car){
  new Model({
    content:car.getSpec();
  }).show();
}

依赖注入

  • 去除代码耦合
class Car {

// 耦合特定实现
  engine = new Engine();
  
  whell = new Whell();
  
  run(){
    this.engine.start();
    this.wheel.run();
  }
}

function main(){
  var car = new Car();
  car.run();
  
}
// 需要使用Inversify.js库或者Nest.js库
class Car {

// 声明依赖
  @inject('engine')
  engine;
  
  @inject('whell')
  whell;
  
  run(){
    this.engine.start();
    this.wheel.run();
  }
}

function main(){
  // 声明实现
  car container = new Container();
  container.bind('engine',Engine);
  container.bind('car',Car);
  container.bind('whell',Whell);
  
  // 得到注入对象
  var car = container.getObject('car');
  car.run();
  
}

五大原则(SOLID原则)

  • 单一职责原则(Single Responsibility Principle)
    • 一个类应该只有一个引起它变化的原因。
    • 例如,一个交通工具类应该只包含关于交通工具的相关属性和方法,而不应该包含其他无关的业务逻辑。
  • 开放封闭原则 OCP(Open - Close Principle)
    • 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,当需要添加新功能时,应该通过增加代码来实现,而不是修改原有代码。
    • 例如,通过继承和多态来实现开发一个插件系统。
  • 里式替换原则 LSP(the Liskov Substitution Principle LSP)
    • 子类应该能够替换掉父类并且任何使用父类的地方都可以使用子类。
    • 例如,一个正方形类和矩形类,矩形类可以派生出一个正方形类,但是如果在使用正方形类的地方,矩形类不能替换正方形类。
  • 接口隔离原则 ISP(Interface Segregation Principle ISP)
    • 客户端不应该依赖于它不需要的接口。即一个类对另一个类的依赖应该建立在最小的接口上,不需要的接口应该剔除。
    • 例如,对于一个包含多个方法的接口,在实现时可以考虑将其拆分成多个小的接口,每个类只依赖于自己需要的接口。
  • 依赖倒置原则 DIP(the Dependency Inversion Principle DIP)
    • 高层模块不应该依赖低层模块,它们都应该依赖于抽象。这意味着具体的实现细节应该被封装起来,而高层模块只需要面向抽象接口编程。

    • 例如,通过使用接口或抽象类来定义一个通用的数据访问层,使得高层业务逻辑不需要关心具体的数据库操作。

      这些原则可以帮助开发人员创建灵活、可维护和可扩展的代码。

  • SRP 可以使类更加可读和理解
  • OCP 可以避免破坏现有代码的更改
  • LSP 可以避免意外的行为
  • ISP 可以避免臃肿的接口
  • DIP 可以减少耦合度。

面向对象编程式的缺点

面向对象编程语言的问题在于,它总是负带着所有它需要的隐含环境。你想要一个香蕉,但得到的却是一个大猩猩拿着香蕉,而且还有整个丛林 - Joe Armstrong(Erlang创始人)

因为是通过类来存储数据和操作,但是有时候我们只需要一个类的一部分功能,但是我们仍不可避免的把整个类引用过来,没办法进行新功能的导入。同时,数据的修改历史完全被隐藏。,是通过一系列补丁来编写程序。

05_函数式编程

  • 函数是“第一等公民”
  • 纯函数 / 无副作用
  • 高阶函数 / 闭包

尽量减少变化的部分,从而使代码更为清晰

一等函数 First Class Function

// 聚合转发
const BlogController = {
  index(posts) {
    return Views.index(posts);
  },
  show(post) {
    return Views.show(post);
  },
  create(attrs) {
    return Db.create(attrs);
  },
  update(post, attrs) {
    return Db.update(post, attrs);
  },
  destroy(post) {
    return Db.destroy(post);
  },
};

// 有了一等函数后
const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
};

纯函数 Pure Function

优势:

  • 可缓存
  • 可移植
  • 可测试
  • 可推理
  • 可并行
const retireAge = 60;
function retirePerson(p){
  if(p.age > retireAge) {
    p.status = 'retired';
  }
}

// 修改为纯函数
function retirePerson(p){
  const retireAge = 60;
  if(p.age>retireAge){
    return {
      ...p,
      status:'retired'
    }
  }
  return p;
}

柯里化 Currying

function add(a,b,c){
  return a+b+c;
}

// 参数重复
add(1,2,5)
add(1,2,4)
add(1,2,7)


// 柯里化之后
const add_ = curry(add);
const add12 = add_(1, 2);

// 之前参数存入闭包
add12(5);
add12(4);
add12(7);

const add1 = add_(1);
add1(2, 5);
add1(3, 5);

function add(a, b, c) {
  return a + b + c;
}

function curry(fn) {
  const arity = fn.length; // 闭包
  return function $curry(...args) {
    if (args.length < arity) {
      return $curry.bind(null, ...args); // 闭包
    }
    return fn.call(null, ...args);
  };
}

组合 Composition

// 手动组合
const toUpperCase = x => x.toUpperCase()
const log = x => console.log;

function alertUppercase(str){
  log(toUpperCase(x));
}

// Composition
const alertUppercaseFn = compose(log,toUpperCase)
const compose = (...fns)=>(...args)=>fns.reduceRight(
  (res,fn)=>[fn.call(null,...res)],args
)[0];

// associativity:compose(f,compose(g,h)) === compose(compose(f,g),h);
// map's composition law: compose(map(f),map(g)) === map(compose(f,g))

Functor

可以当作容器的类型,类型支持对容器内元素进行操作

常见的 functor:Array(Iterable).map,Promise.then

// 一般判断(吐槽一下,真的会有人这么写么)
a.b != null ? (a.b.c != null ? (a.b.c.d !== null ? a.b.c.d.e : null) :null) :null;

// Functor
class Maybe{
  constructor(x){
    this.$value = x;
  }
  map(fn){
    return this.$value == null ? this : new Maybe(fn(this.$value));
  }
}

new Maybe(a).map(prop('b')).map(prop('c')).map(prop('d')).map(prop('e'));

Monad

可以去除嵌套容器的容器类型

常见monad:Array.flatMap Promise.then

[1,2].flatMap(()=>([1,2]));
Promise.resolve(1).then(r=>Promise.resolve(2*r))

// Monad
Maybe.prototype.flat = function(level = 1) {
  if(this.$value?.constructor !== Maybe){
    return new Maybe(this.$value);
  }
  return level ? this.$value.flat(--level) : this.$value
}

Applicative

直接对两个容器进行操作

new Maybe(2).map(two => new Maybe(3).map(add(two))).flat(); // Maybe(Maybe(5))

// Applicative
new Maybe(add).ap(new Maybe(3)).ap(new Maybe(2));

class Maybe {
  constructor(x) {
    this.$value = x;
  }
  map(fn) {
    return this.$value == null ? this : new Maybe(fn(this.$value));
  }
  ap(other) {
    return other.map(this.$value);
  }
}
// Applicative 的规则
// 一致性规则 Identity: Maybe(id).ap(v) === v
// 重构性规则 Homomorphism: Maybe(f).ap(Maybe(x)) === Maybe(f(x));
// 可交换规则 Interchange: v.ap(Maybe(x)) === Maybe(f => f(x)).ap(v);
// 组合规则   Composition: Maybe(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w))

06_响应式编程

RxJS

  • 异步/离散的函数式编程
    • 数据流
    • 操作符
      • 过滤
      • 合并
      • 转化
      • 高阶

Observable

  • 观察者模式
    • 需要订阅获取到数据
  • 迭代器模式
    • 数据会不断的推送过来,而不是一次性的
  • Promise/EventTarget 超集*
    const { fromEvent } = rxjs;
    
    const clicks = fromEvent(document,'click');
    
    const sub = clicks.subscribe(x => console.log(x))
    
    setTimeout(()=>sub.unsubscribe(),5000)
    

操作符

响应式编程的 “compose”

  • 合并
  • 过滤
  • 转化
  • 异常处理
  • 多播
const { fromEvent, map } = rxjs;
const clicks = fromEvent(document, "click");

// 转化式编程
const positions = clicks.pipe(
  map(ev => ev.clientX), // 转化操作符
  map(clientX => ++clientX)
);
positions.subscribe(x => console.log(x));

//
const events = [{ clientX: 1 }, { clientX: 2 }];
compose(
  map(clientX => ++clientX),
  map(ev => ev.clientX)
);

Monad

去除嵌套的

const { fromEvent, flatMap, fetch } = rxjs;
const clicks = fromEvent(document, "click");
const users = clicks.pipe(
  flatMap(e => fetch.fromFetch(         // 嵌套 Observable
    "https://api.github.com/users?per_page=5"
  )),
  flatMap(res => res.json()) // 嵌套Promise
);

users.subscribe(data => console.log(data));

总结

编程范式过程式自顶向下
结构化编程
问题
面向对象封装
继承
多态
原则
问题
函数式编程一等函数
纯函数
curry/compose/functor/monard/applicative
响应式编程observable
操作符
monard

领域特定语言 —— 了解即可

什么是领域特定语言

  • Domain-specific language(DSL):应用于特定领域的语言
    • HTML
    • SQL
  • General-purpose language :通用语言
    • C/C++
    • Javascript

lexer

SQL Token 分类

  • 注释
  • 关键字
  • 操作符
  • 空格
  • 字符串
  • 变量

Parse_语法规则

上下文无关语法规则

<selectStatement> ::= SELECT <selectList> FROM <tableName>
<selectList> ::= <selectField> [ , <selectList> ]
<tableName> ::= <tableName> [ , <tableList> ]
  • 推导式:表示 非终结符 到 (非终结符或终结符) 的关系
  • 终结符:构成句子的实际阵容。可以简单理解为词法分析中的token
  • 非终结符:符号或变量的有限集合。他们表示在句子中不同类型的短语或子句

Parse_LL

LL:从左到右检查,从左到右构建语法树

Parse_LR

LR:从左到右检查,从右到左构建语法树

tools

npx kison -m llk -g grammar.js -o cal.js

总结

编程范式编程语言机器语言
汇编语言
中级语言C
高级语言C++
Lisp
JavaScript
编程范式什么是编程范式
过程式编程范式
面向对象编程
函数式编程
响应式编程
领域特定语言创造 DSLlexer
parser
tools
visitor