04-编程范式 | 青训营笔记

46 阅读21分钟

42_Salvation_4k.jpg

  1. 本文共10800+字符
  2. 阅读建议:需要有至少 C C++ JS的编程基础,对于编译器知识或者抽象语法树有一定的耳闻。
  3. 本文主要介绍 基本编程语言,编程范式(过程、对象、函数式、响应式)、如何创造一门DSL语言

00 课程介绍

课程背景

Pasted image 20230417015309.png

课程收益

Pasted image 20230417015332.png

01 编程语言

  • 为什么需要编程语言——与机器沟通

机器语言

  • 典型的8086

汇编语言

  • 使用 助记符 代替一定的机器代码

高级语言

  • 更方便,贴近自然语言
  • 出现编译器

C/C++

  1. C “中级语言” 过程式语言代表
    1. 可对位、字节、地址进行直接操作
    2. 代码与数据分离,倡导结构化编程
    3. 功能齐全:数据类型与控制逻辑多样化
    4. 可移植能力强
  2. C++ 面向对象语言代表
    1. C with Classes
    2. 继承
    3. 权限控制
    4. 虚函数
    5. 多态

Lisp

  1. 函数式语言编程代表
  2. 完全屏蔽指针概念
  3. 机器无关
  4. 出现了闭包的概念
  5. 列表:代码即数据。

Javascript

  1. 基于原型与头灯函数的多范式语言
    1. 过程式
    2. 面向对象
    3. 函数式
    4. 响应式*

总结

Pasted image 20230417015823.png

03 编程范式

什么是编程范式

  • 根据一些特性对语言进行分类

Pasted image 20230417015850.png

常见的编程范式

Pasted image 20230417015914.png

  • 命令式:程序员如何操作及其改变状态
    • 面向过程:根据过程进行分组
    • 面向对象:根据对象及其操作进行分组
  • 声明式:程序员声明想要的结果,而不指明具体的操作
    • 函数式:通过一系列函数组合生成逻辑
    • 响应式:通过数据流和映射函数产生结果

面向过程

  • 典型的数据结构与算法就出现在 面向过程,主要特性以下两点
  1. 自顶向下

Pasted image 20230417020313.png

  1. 结构化编程

Pasted image 20230417020337.png

  • 面向过程的编程的最大贡献-取消了goto
  1. JS中同样具有 面向过程

Pasted image 20230417020419.png

  1. 面向过程的问题
    1. 数据与算法关联弱
      1. 在结构化编程中,函数与其操作的数据之间没有清洗与直观的体现,随着程序规模的增大,程序变得难以理解与维护,很难一下看出来函数之间的调用关系,某些函数调用哪些数据,某些数据由哪些函数使用,很难发现关系,在这种情况下,有数据或者函数出问题,很难找出,程序查错变得困难
    2. 不利于修改与扩充
      1. 增加新功能
      2. 面向对象没有封装的概念,要访问数据可以直接访问
      3. 要修改一个访问变量要一格一格去找修改
    3. 不利于代码重用
      1. 结构化编程中要抽取类似函数的功能非常困难

面向对象

  • 思想
    • 通过类将数据封装,只能通过提供的方法进行数据操作
    • 限制了数据的访问方式
  • 特点
    • 封装
      • 客观事物封装为具体的类
      • 数据对可信对象可以操作,对不可信的进行隐藏
      • 一个类就是封装了数据与操作数据的代码的逻辑形式
      • 防止类中无关变量的意外改变
      • 进行数据保护
    • 继承
      • 可以让某个类型对象获得另一个类型对象的属性与方法
      • 支持???概念?没听清楚
      • 可以使用现有类的所有功能,在原有类的基础上进行扩充
      • 扩充中不需要对原有类进行改变
      • 无需重写的情况下进行功能扩充
    • 多态
      • 一个类的实现方法在不同的情况下有不同的表现形式
      • 使得内部结构不同的对象可以使用相同的外部接口
      • 这意味着 不同的类 可以使用公共的接口进行调用
      • 在新类中覆盖子类的函数,在调用时不需要考虑是基类还是子类
      • 不同的结构可以进行接口共享,进而达到函数复用
      • 接口复用与函数复用
    • 依赖注入*(本质上可以参考下一章节内容 [[05-客户端容器 web浏览器以及跨端方案#跨端容器-WebView]] 其实是Native与前端框架的各自封装接口,再通过相互通信达到调用功能的能力。
      • 去除代码耦合,解决对象之间的依赖关系
依赖注入(Dependency Injection,简称 DI)是面向对象编程中的一种设计模式,用于解决对象之间的依赖关系问题。在 DI 模式中,对象的依赖关系不是由对象本身来创建和管理,而是由外部容器来负责创建和管理。

具体来说,依赖注入模式可以分为三种方式:

1.  构造函数注入(Constructor Injection):在对象的构造函数中通过参数传递依赖对象。
2.  属性注入(Property Injection):通过对象的属性来传递依赖对象。
3.  接口注入(Interface Injection):在对象中定义一个接口,并通过接口方法来传递依赖对象。

依赖注入具有以下优点:
1.  降低对象之间的耦合性:通过将对象的依赖关系转移到外部容器中,可以降低对象之间的耦合性,使得对象更加灵活、可维护。
2.  提高代码复用性:通过将依赖对象提取到外部容器中,可以提高代码的复用性,避免重复创建相同的对象。
3.  提高代码的可测试性:通过将依赖对象传递给对象的参数或属性中,可以更方便地对对象进行单元测试,避免测试过程中产生不必要的依赖关系。
4.  提高代码的可读性:通过在对象中明确注入的依赖关系,可以更加清晰地了解对象之间的关系,提高代码的可读性和可维护性。
    
需要注意的是,依赖注入模式并不是一种万能的设计模式,它也存在一些缺点,比如增加代码的复杂度、增加了依赖关系的传递过程等。因此,在使用依赖注入模式时需要根据具体情况进行合理的选择和使用。

Pasted image 20230417022144.png

  • 面向对象编程-五大原则
    • 单一职责原则 SRP
    • 开放封闭原则 OCP
      • 扩展性方面开放,更改性方面封闭
    • 里氏替换原则 LSP
      • 子类可以替换父类并可以出现在父类出现的任何地方
    • 依赖倒置原则 DIP
      • 具体依赖抽象
      • 上层依赖下层
        • b比a低,b需要a,b定义接口,a实现,b解除了对a的依赖,当我们需要替换a的时候,可以直接替换
    • 接口分离原则 ISP
      • 任何时候依赖接口,不要依赖具体实现
  • 面向对象编程的缺点
    • Pasted image 20230417022622.png
    • 无法进行细功能的功能导入
    • 数据修改历史完全被隐藏了
    • 面向对象提供了一种持续写烂代码的方式,不断地补丁拼凑程序,有些偏激

函数式编程

函数式编程(Functional Programming,简称 FP)是一种编程范式,主张使用函数来进行编程。函数式编程强调将计算过程看做是数学上的函数计算,避免了程序状态和变量的改变,强调使用不可变数据和无副作用的函数,以及将函数作为一等公民进行处理。

函数式编程的特点包括:
1.  函数是一等公民:函数可以作为参数传递给其他函数,也可以作为返回值返回给调用者。
2.  不可变数据:函数式编程中的数据是不可变的,即一旦创建不可修改,这样可以避免多线程环境下的并发问题。
3.  无副作用的函数:函数式编程中的函数不会对程序状态产生影响,即函数的输出只和输入有关,不会对环境进行改变。
4.  高阶函数:函数式编程中的函数可以接受其他函数作为参数,也可以返回其他函数作为结果。
    
函数式编程主要适用于解决复杂问题和高并发场景,它具有以下优点:
1.  可读性强:函数式编程代码通常更加清晰、简洁,容易理解。
2.  易于测试:由于函数式编程中函数不涉及状态和环境,因此容易进行单元测试。
3.  并发性强:函数式编程中的无副作用的函数和不可变数据可以避免并发问题,提高并发性能。
4.  易于推理:函数式编程中的函数都是确定性的,不会产生副作用,因此容易进行代码推理和优化。
    
需要注意的是,函数式编程并不是一种适用于所有场景的编程范式,它对编程语言和编程习惯都有一定的要求,需要根据具体情况合理选择。此外,函数式编程也存在一些缺点,比如可读性不如面向对象编程,不适用于所有场景等。

Pasted image 20230417022959.png

  • JS 并不是纯粹的函数式编程
  • 可以使用 一些库加上编程技巧实现 推荐库 ramdajs

First Class Function

  • 比如API的封装转发
  • Pasted image 20230417023405.png

Pure Function

  • 不依赖外部环境
  • 没有副作用
  • 优势
    • 可缓存
    • 可移植
    • 可测试
    • 可推理
    • 可并行
  • Pasted image 20230417023500.png

Currying

  • 利用函数与高阶函数解决 参数每次传来传去的麻烦,并且延迟传播参数的准备工作
  • Pasted image 20230417023729.png
  • 实现利用 闭包 高阶函数的特性
    • 将之前调用的参数暂存起来,同时生成一个新的函数
    • Pasted image 20230417023807.png
    • 新韩淑被调用时,将之前的参数与现在的参数合并起来调用最开始的函数

Composition

  • 鼓励 函数组合
  • 通过函数组合来实现更为复杂的功能
  • 手动组合
    • Pasted image 20230417023914.png
    • 灵活性差
  • 通过动态运行时组合
    • 处理时数据从左流入,处理之后返回
    • Pasted image 20230417024008.png
    • Pasted image 20230417024413.png
    • 上面的例子 前面采用了交换律 后面采用了分配律,这样可以减少一次函数调用
    • 利用了函数是一等公民与高阶函数的概念
函数式编程中,composition(组合)是一种用于组合函数的技术,通过将多个函数按照一定规则组合起来,构建出新的函数。这种技术可以使函数的复杂度得到简化,降低出错的风险,提高代码的可读性和可维护性。 
具体来说,函数式编程中的 composition 技术有两种形式:
1. 函数串联(Function Chaining):将多个函数串联起来,将一个函数的输出作为另一个函数的输入,构建出新的函数。通常使用符合操作符 `.` 或者 `>>` 进行串联。
例如: ``` f . g . h ``` 
上面的代码表示将函数 `h` 的输出作为函数 `g` 的输入,将函数 `g` 的输出作为函数 `f` 的输入。 
2. 函数嵌套(Function Nesting):将多个函数嵌套起来,将一个函数的输出作为另一个函数的输入,构建出新的函数。通常使用函数调用符号 `()` 进行嵌套。
例如: ``` f( g( h(x) ) ) ``` 
上面的代码表示将函数 `h` 的输出作为函数 `g` 的输入,将函数 `g` 的输出作为函数 `f` 的输入。 

使用 composition 技术可以将多个简单的函数组合成复杂的函数,降低函数的复杂度,提高代码的可读性和可维护性。同时,还可以避免出错的风险,因为每个函数都是独立的,不会影响其他函数的执行结果。 需要注意的是,函数式编程中的 composition 技术需要遵循一定的规则,比如函数的输入和输出类型需要匹配,函数的参数需要满足结合律和交换律等。同时,在组合函数时需要注意避免过度嵌套和过度复杂,保持代码的简洁和可读性。
  • Functor
    • 可以当作容器的类型
    • 容器支持对容器内的元素进行操作
    • JS中常见的Functor: Array(Iterable).map Promise.then
    • 容器的好处在于对容器内的元素进行原子操作不必考虑异常情况?将这种操作保存在容器中
    • Pasted image 20230417025038.png
    • 上图中,在原本取值时需要判断空,在容器化之后,就可以直接访问,在容器中进行判空操作即可
函数式编程中,Functor 是一个接口或者类型类,定义了一些基本操作,可以将其用于函数组合、映射、过滤等操作。Functor 是一种能够被映射的对象,它可以是一个容器,也可以是一个函数,只要它满足一些特定的条件。

具体来说,Functor 满足以下两个条件:
1.  实现了 `map` 函数:`map` 函数接收一个函数作为参数,将 Functor 中的每个元素都传递给这个函数,并返回一个新的 Functor。
2.  遵循函数组合律:Functor 的 `map` 函数遵循函数组合律,即对于任意的 Functor `f`、函数 `f`、 `g` 和 `h`,都满足以下公式:

f.map(g).map(h) == f.map(x => h(g(x)))

其中,`f.map(g)` 表示先将 `f` 中的每个元素映射到 `g` 中,然后再将结果映射到 `h` 中,最终得到一个新的 Functor。而 `f.map(x => h(g(x)))` 表示先将 `f` 中的每个元素映射到 `g` 中,然后再将结果映射到 `h` 中,最终得到一个新的 Functor。
Functor 的一个重要应用是在函数组合中。通过将多个 Functor 组合起来,可以构建出复杂的函数,这些函数可以应用于多种场景,包括数据转换、数据过滤、异步处理等。
需要注意的是,Functor 的实现方式和语言有关,不同的编程语言实现方式可能有所不同。同时,由于 Functor 的特性,它也存在一些限制和缺点,比如可能会导致性能问题,需要在实际应用中进行权衡。

Monad

  • 可以去除嵌套容器的容器类型
  • 常见Monad : Array.flatMap Promise.then
  • 有可能一个容器内部还嵌套了容器,我们更关心内部的数据元素,所以需要去除嵌套容器,如果这个容器支持嵌套容器,我们称为 Monad
  • Pasted image 20230417025408.png
  • 上面是打平操作的一个样板,通过递归的进行打平操作,最终返回一个容器内直接是数据元素,而不是容器嵌套容器的场景

Applicative

  • 直接对两个容器直接操作
  • 函数式编程中,数据常常在容器中,所以很多时候需要对容易进行操作
  • 支持对两个容器进行直接操作,就称为 Applicative
  • Pasted image 20230417025809.png
  • 如果不支持 Applicative,则需要取出数据巴拉巴拉再进行打平操作?为了更方便操作
  • 支持 Applicative 的,有更多良好的数学性质
    • Identity
    • Homomorphism
    • Interchange
    • Composition
函数式编程中,Applicative 是一种用于表示可应用的 Functor 的类型类。它是 Functor 的一种扩展,提供了一些额外的功能,可以实现函数的组合和复杂操作。Applicative 是一种更加抽象的概念,通常与 Functor、Monad 等一起使用。

具体来说,Applicative 主要包括以下两个方法:
1.  `apply` 方法:`apply` 方法接收一个 Functor 和另一个 Functor 中的函数,将函数应用到第一个 Functor 中的每个元素上,返回一个新的 Functor。
2.  `pure` 方法:`pure` 方法接收一个值,将值封装为一个 Functor 返回。

Applicative 的 `apply` 和 Functor 的 `map` 功能类似,都可以用于将一个函数应用到一个 Functor 中的每个元素上,但它们之间的区别在于 `apply` 可以接受多个 Functor,同时将多个 Functor 中的函数应用到多个 Functor 中的元素上。而 `pure` 方法则可以用于将一个值封装成一个 Functor,以便将其用于 `apply` 操作。

通过 Applicative,可以构建出复杂的函数,可以用于多种场景,例如数据转换、数据过滤等。它可以用于简化代码,提高代码的可读性和可维护性,同时也可以避免出错的风险。

需要注意的是,Applicative 是一种更加抽象的概念,通常需要结合 Functor、Monad 等概念一起使用,需要在实际应用中进行权衡。同时,不同的编程语言对于 Applicative 的实现方式也可能有所不同。
  • 什么是打平操作?
在函数式编程中,打平操作(Flatten)是指将一个嵌套的数据结构(如列表、数组、对象等)展开成一个扁平的序列。打平操作通常用于处理嵌套的数据结构,从而简化数据处理和操作。 具体来说,打平操作通常包括以下几个步骤: 
1. 遍历数据结构:首先需要遍历嵌套的数据结构,找到其中的所有元素。 
2. 判断元素类型:对于每个元素,需要判断其类型,如果是基本类型(如数字、字符串等),则将其添加到结果序列中;如果是嵌套的数据结构,则需要递归地进行打平操作。 
3. 合并结果:最后需要将所有的元素合并成一个扁平的序列,并返回结果。 在JavaScript中,可以使用Array.prototype.flat()方法来实现打平操作。该方法可以将一个嵌套的数组展开成一个扁平的数组,可以指定展开的深度,也可以不指定深度,直接展开所有嵌套的数组。 
除了使用Array.prototype.flat()方法外,还可以使用递归函数来实现打平操作。递归函数可以遍历嵌套的数据结构,对于每个元素,判断其类型,如果是基本类型,则将其添加到结果序列中;如果是嵌套的数据结构,则递归调用自身,将返回的结果合并到结果序列中。 需要注意的是,在进行打平操作时,需要考虑到性能和内存占用问题。如果数据结构嵌套层数较深,打平操作可能会消耗大量的内存和计算资源,从而影响程序的性能。因此,在实际应用中需要根据具体情况进行权衡和选择,选择合适的打平方法和参数,从而实现数据结构的扁平化处理。同时,需要注意避免出现死循环、栈溢出等问题,保证程序的稳定性和安全性。

响应式编程

概念

  • 在 异步/离散 情况下的函数式编程
  • 主要包含两个概念
    • 数据流
    • 操作符
      • 过滤
      • 合并
      • 转化
      • 高阶
  • 没有纯粹的响应式编程语言,需要工具库 RxJS 帮助
  • 一个例子
    • Pasted image 20230417030705.png
      • excel最后的求和函数无论前面输入的参数值是来自哪个参数位置,都可以进行计算,每个格子的地位平等,可以任意组合产生强大功能

Observable

  • 观察者模式
  • 迭代器模式
  • Pasted image 20230417030932.png
  • 观察者模式类似于右边的订阅
  • 迭代器模式类似于持续不断的推送数据
  • 在前端中类比为 Promise/EventTarget 超集 *
    • Pasted image 20230417031033.png

操作符 Compose

  • 进入的数据实际上已经进行过类似于 管道中的多级操作了
    • Pasted image 20230417031224.png
  • 操作符有很多类 响应式编程的 Compose
    • 合并
    • 过滤
    • 转化
    • 异常处理
    • 多播
  • 例子
    • Pasted image 20230417031302.png
    • 上面左边的输入事件数据经过转换,最后得到了统一的坐标数据
    • 可以类比为函数式编程中的组合概念,只不过函数式的组合是针对同步数据,响应式编程中针对的是离散异步的数据

Monad

  • 监听的数据流 Obsever 嵌套的?
  • Pasted image 20230417031523.png
  • 在pipe中对容器进行去除

总结

Pasted image 20230417031551.png

04 领域特定语言 DSL

DSL 概念

Pasted image 20230417031627.png

  • DSL一般是通过通用语言实现的
  • 通用语言不能通过领域特定语言实现

语言运行机制

Pasted image 20230417031852.png

  • code gen 解释器
  • interpreter 编译器

创造 DSL

lexer

  1. 制定词法规则
  • 词法解析器
  • 切分为token
  • 一般是通过正则表达式进行切分操作
  • Pasted image 20230417032021.png
  • Pasted image 20230417032033.png

Parser 语法规则

  1. 根据词法规则指定语法规则
  • 首先指定上下文无关语法
  • Pasted image 20230417032121.png
  • 什么是上下文无关语法?
上下文无关语法(Context-Free Grammar)是一种形式语言,它的基本思想是将语言的语法规则表示为一组产生式(Production),每个产生式包含一个非终结符(Non-terminal)和一组终结符(Terminal)。通过这些产生式,可以生成符合语法规则的句子(Sentence)。 
具体来说,上下文无关语法通常包括以下几个部分: 
1. 终结符:终结符是语言中的基本元素,它表示语言中的单词、符号等。终结符通常用小写字母或者符号表示。
2. 非终结符:非终结符是语言中的复合元素,它表示语言中的短语、句子等。非终结符通常用大写字母表示。 
3. 产生式:产生式是语言中的语法规则,它表示如何将一个非终结符替换为一组终结符或者非终结符。产生式通常采用“非终结符 -> 终结符或非终结符”的形式表示。 
4. 开始符号:开始符号是语言中的起始符号,它表示语言中的最高级别的非终结符。通过开始符号,可以生成符合语法规则的句子。 
上下文无关语法具有以下几个特点: 
1. 产生式的左部只能是一个非终结符,右部可以是一组终结符或者非终结符。 
2. 产生式之间是相互独立的,即产生式的左部和右部不受上下文的影响。 
3. 产生式可以递归定义,即一个非终结符可以通过一组产生式来表示。 

上下文无关语法在计算机科学中有广泛的应用,比如编译器、自然语言处理、图像处理等领域。在编译器中,上下文无关语法通常用于描述程序的语法结构,从而实现程序的解析和编译;在自然语言处理中,上下文无关语法通常用于描述自然语言的语法结构,从而实现自然语言的分析和理解;在图像处理中,上下文无关语法通常用于描述图像的结构和特征,从而实现图像的识别和分类。 需要注意的是,上下文无关语法并不能描述所有的语言,有些语言需要上下文有关语法(Context-Sensitive Grammar)来描述。因此,在实际应用中需要根据具体情况进行选择和设计,从而实现语言的描述和处理。

Parser LL

  1. 产生语法分析器-手动
  • 语法分析的两种方式之一
  • Pasted image 20230417032213.png
  • 类比自顶向下的函数调用
  • 适用于一些简单语法,自己实现语法解析器的话

Parser LR

  1. 产生语法分析器-工具
  • 表达能力强,适应性好
  • 坏处是,不适合手写,适合使用工具生成
  • Pasted image 20230417032346.png
  • 最后的K和1是向下看的token的数量

tools->语法分析器->语法树

  1. 产生语法树

Pasted image 20230417032623.png

Pasted image 20230417032653.png

visitor

  1. 遍历语法树识别

Pasted image 20230417032715.png

总结

Pasted image 20230417032802.png

本节内容主要是介绍了三大模块,基本语言介绍、编程范式介绍、以及如何创造一门DSL语言

  1. 基本语言介绍
  2. 编程范式:主要讲解后两种 函数式编程与响应式编程。这部分没有函数式编程与响应式编程的经验很难理解说的是什么以及有什么样的好处。我目前的理解是,函数式编程它实现了类似于数学函数的性质,代码的可读可维护可重用性更好。响应式编程主要在函数式的基础上实现了异步与离散的兼容。
  3. 创造 DSL:主要流程是,自己制定词法规则,根据词法规则指定上下文无关的语法规则,使用工具生成LR语法解析器(也可以LL手动生成),生成的语法解析器用于生成语法树,遍历语法树识别放入解释器或者编译器得到运行。

参考

Pasted image 20230417033047.png