Out_of_the_Tar_Pit

  • https://aistudio.google.com/prompts/1EXqCgJOEDAeEmaT6oMkgYLHXtrb-bSMo
  • 背景是 OOP 以及对应的语言 Java/C++ 统治了企业级的开发,随着规模的扩张软件的理解和修改越发困难
    • 主要的问题是 shared mutable state 以及 side effect
  • tar pit 用于比喻大型软件项目因为自身复杂性陷入难以维护的困境
  • 和 mythical man-month 也有关联
  • 核心论点是软件开发的最主要敌人是 complexity
    • 分为 essential/accidnetal complexity,软件工程师应该消灭前者
    • complexity 主要来源于 state 和 control flow(软件的逻辑/执行顺序很难追踪)
  • 解决方案是 FRP: functional, relational programming
    • functional core 负责完成业务逻辑
      • 需要保证数据都是不可变的,函数没有任何 side effect
      • 使用类似 SQL 的 relation 方式处理数据
    • inperative shell 负责和外界(包括用户、数据库/网络、系统状态)交互
      • shell 应该尽可能地薄
  • 最终推动了 FP 的(一定程度上的)复兴

1-3 Introduction, complexity and approaches to understaning #

  • https://aistudio.google.com/prompts/1QTv2LWO-RYtnGbHelDKUrNjEhrA-h_ZV
  • 1968 年提出的 software crisis 仍然存在,问题的根源在于 complexity,而 complexity 来自 state, code volume 以及 flow of control
  • 两种主流的 coding 方式的处理策略是
    • OOP 的做法是将可变状态和相关的行为封装在一起,控制状态的访问范围
    • FP 的做法是完全避免可变状态和 side effect
  • no silver bullet 提到软件工程的四个难题是 complexity, conformity, changeability, invisibility
    • 但是这篇文章认为 complexity 是最本质的,其他三者都是 complexity 的某种特殊表现
    • 最根本的问题是:软件过于复杂导致开发者无法 理解 系统,无法预见所有的行为和修改带来的后果、无法长时间地开发和维护
    • 引用了一些 Turing award lecture 的内容,总之 simplicity is hard
  • 两种试图理解系统的方法是 testing 和 informal reasoning
    • testing 是一种黑盒方法:只要系统在特定输入下表现出符合预期的输出,整个系统就是正常的
    • informal reasoning 是一种白盒方法:我们通过检查软件设计来理解其工作原理
    • informal reasoning 是更加重要的,因为
      • testing 始终是有限的:可能的输入组合是天文数字
      • informal reasoning 是始终存在、自动发生的
      • informal reasoning 可以从源头上减少被创造出的错误,testing 只能让已存在的错误被发现
        • testing 只能证明缺陷存在,无法证明缺陷不存在
  • 一个简单的系统会让所有试图理解它的努力都更加有效,所以应当将提升系统 simplicity 作为首要任务

4 Causes of complexity #

  • https://aistudio.google.com/prompts/13ZxGt96H7NGk0kWFzLfd3Trkmc6D1hrL
  • state 是一个重要的复杂性来源(无法枚举),state 使得程序难以理解并且可靠性变低
    • 对测试的影响在于,在一个状态下进行的测试无法证明另一个状态下系统的行为
    • 对于 informal state 的影响同样严重:state 是无法穷尽的,心智模拟变得不切实际
      • 只要某个无状态的程序调用了一个 stateful 的程序,那么我们就必须在整个系统的状态上下文下去分析它
  • 第二个来源是 control 和 concurrency
    • 大部分编程语言强制要求通过文本顺序来显式指定程序的执行顺序(这是一种 over-specification),比如程序员一般不关心三个变量的赋值先后顺序,但是三个赋值语句在代码中是有文本的先后顺序的
    • concurrency 的问题在于 testing 每次得到的结果不一定一样,并且也导致 informal reasoning 的难度上升
  • code volume 是一个 secondary effect
    • 这种 complexity 容易被量化
    • 和其他的 complexity 会产生恶性的交互
    • 一般来说复杂性随 code volume 是非线性(超线性)增长的(citation 仍然是 no silver bullet)
      • 通过合适的抽象(或者说有效地管理 state/control 等复杂性根源),理解程序的智力投入可以做到和 code volume 成比例增长
  • 其他的复杂性来源包括 duplicated/dead code, unnecessary/missed abstraction, poor modularity, poor documentation
    • 不同来源的 complexity 之间是相互滋生的,比如开发人员因为难以理解当前系统重新开发功能从而引入新的复杂性
    • simplicity is hard,尤其是面临时间压力/当前系统已经很复杂的情况下
      • simplicity 是一种需要被认识、追求和珍视的品质,它不会自动出现
    • 编程语言能力越强/自由度越高,系统越难以理解
      • 最好在语言层面加入限制和保证,比如不允许进行手动内存管理
      • 需要警惕任何允许 state 存在的语言

5 Classical approaches to managing complexity #

  • https://aistudio.google.com/prompts/16rPTG8T9RFvFZJHIGn7oL5W5qlMNssiF
  • OOP 本质上是一种命令式的编程方法,很多特性都源自状态计算的需求
    • 一个 object 由一些 state 和一组用于访问和操作 state 的方法构成,通过「对状态的访问和修改只能通过方法进行」(封装)来保证对状态的约束
      • 缺点在于多个方法、多个对象情况下约束不再有效
    • 另外一个缺陷在于:和关系代数的原则不同,两个 state 一样的 object 也被识别为两个实体
      • 当对象用于某些 immutable 的量的时候(也就是 obejct 概念不再适用,需要数值等价性),只能采用类似 value object 的方式作为临时解决方式
    • 总之所有 OOP 都不可避免地依赖于对象内部的状态,无法为避免复杂性提供一个充分的基础
  • FP 根植于无状态的函数计算,可以分为 pure functional 和 impure 两类
    • 一个好处是函数在给定参数下总是返回相同结果,对于 testing 非常有帮助
      • 因为影响参数结果的所有因素都在参数中,所以 informal reasoning 也非常有效
    • 通过 fold/map 等高阶函数可以解决 flow control 相关的问题
    • 对 state 的处理方式是通过传递额外参数来模拟状态变化,比如给 get_next_counter 增加额外的 old_counter 作为参数
      • 将隐藏的可变状态转换为函数的输入和输出,保证了 referential transparency
    • 体现了 state/function 两种 modularity 的思想:state/OOP 的优势在于不修改 caller 代码的情况下改变 object 状态,FP 的优势在于可以精确知道函数执行的过程和输出对输入的依赖关系(但是 caller 的代码需要同步地修改)
      • FP 将难度放到了 coding 一侧,从而降低未来的阅读/推理/测试的难度
    • 当系统必须处理某种状态时,FP 的弱点就被暴露出来,可以用一些补丁进行弥补(Haskell monad)
  • logic programming 是一种声明式的编程风格,和 stateful von-Neumann style 并不一致
    • 只需要对问题和期望的解决方案作出描述(通过一系列公理),之后由逻辑编程系统自动寻找方案
    • 一个代表是 prolog 语言
    • 许多 logic programming 提供了对 state 的支持,引入便利性的同时也引入了问题
    • 在解决 flow control 的问题上最有前景

6 Accidentals and essence #

  • https://aistudio.google.com/prompts/1Ru1IMJdJeN6WWyYHvZ9AX1gPRKUILLCz
  • 两种 complexity 来自 no silver bullet 文章
  • essential complexity 是从用户视角出发看待的复杂性,是在解决方案构建之前存在于领域中的内在逻辑的复杂性
    • 甚至来自 bit/byte/transistor/electricity 的问题/复杂性都不是 essential 的,即使是基础的元素也是 solution 的一部分而非问题的一部分
  • accidental complexity 是开发者 本不需要 处理的复杂性,比如为满足性能要求引入的复杂机制
    • 实际世界中仍然要承认存在和应对 accidental complexity
  • 对 Brooks 的部分反驳:软件的复杂性是可以消除的,属于 accidental complexity
  • 目标应当是尽可能消除 accidental complexity,或者至少帮助开发者更好地处理

7 Recommended general approach #

  • https://aistudio.google.com/prompts/19csjiLSLToFQD562XmJ0ViFYlXNwfrAV
  • 在 ideal world 中审视哪些 complexity 是无法避免的
    • 第一步是将用户的 informal requirements 转化为 formal requirements(消除歧义和遗漏),这里的 formal requirements 还没有考虑任何执行过程
    • accidental complexity 存在于 formal requirement 和 execution 之间
    • 类似于 declarative programming 的过程
    • 数据可以分为 input 和 derived 两类,其中 derived 包括 immutable/mutable 两类(tab1)
      • 如果系统有概率会用到这些 input data 则属于 essential,否则属于 accidental
      • immutable derived data 总是可以从 input data 中计算得出,所以是 accidental 的
      • mutable derived data 也可以从 input data 计算得出,并且其修改可以视作对 input data 的修改
      • 类似 cache 的数据也属于 accidental
    • control flow 是完全 accidental 的,开发者不应该关心 control flow(以及 concurrency)
      • 系统运行的结果不应该和具体控制机制相关
  • 现实世界和理想的差距在于 表达效率/能力性能需求
    • 要求表达是「可执行的」本身会限制表达能力,此外可执行的表达也不一定是高效率的
      • 有时 control flow 的作用是提升程序效率
      • cache 是一个非常典型的例子
    • 很多情况下 state 是最便利的对现实世界建模的方式,比如游戏对战
      • 具有 essential 性质的做法是:任何时刻的位置/属性都由初始位置和用户的操作历史推导出来
  • 从最小化 complexity 的目标出发,建议的做法是 avoid 和 separate:避免非 essential 的 state/control,以及将不可避免的 accidental complexity 和系统的其他部分清晰隔离开
    • 两个原则虽然并不新颖,但是没有成为软件开发的主流
    • 对于「加入 accidental state 可以提升性能」的情况,应该放弃手动的管理,仅仅声明哪些 state 是必要的,将具体维护交给一个独立的基础设施来完成
    • 对于表达便利性的问题,应该 pretend 这个 accidental state 是 essential 的,比如将其视作 user input
    • 分离的原则是 logic/state 分离以及 accidental/essential 分离
      • 所以一个系统应该由三层组成(fig1)
        • essential state 完全自包含
        • essential logic 表达规则,引用 essential state,其变化不影响 essential state
        • accidental state/control 引用以上两层,其变化不会影响以上两层,移除此层不会影响系统工作而仅影响速度/效率
      • 这样在思考 essential logic 的时候不需要考虑外层的 accidental component
    • 设计初期为性能作出的复杂性方面的妥协最后会导致灾难性的后果,优化一个简单的系统比优化复杂系统要容易很多

8 The relational model #

  • https://aistudio.google.com/prompts/1Z9BEzgr04hU_bAg2v2FyuAeAakLJ5YQI
  • 关系模式的思想来源于数据库管理,但是作者 claim 实际上其不具有专门性,而是应当作为结构化/操作数据的一种通用的方法
    • 一个软件工程的共识是 SQL 并非关系模型的准确体现(SQL 实际上是一种工程上的妥协)
  • 关系模型的核心在于 structure (relation), manipulation, integrity, data independence 四个方面
  • relation 被定义为一个同质的 record 集合,每一条记录本身则是一个由 attributes 组成的异质集合
    • 和 table 的区别在于:不允许包括重复 record,record 之间没有顺序区分
    • 可以分为 base relation 和 derived relation
    • Chris Date 的观点是将关系视作变量(relation variables)
  • relation 的一个优势是和 access path 无关
    • 层次模型和 access path 相关:两个数据相互之间的包含关系会决定两个方向中哪一个方向的查询是更高效的
    • 网络模型也是 access path dependent 的:主要/已被预见到的检索需求相对高效,而次要检索需求非常低效
    • OOP/XML 也有类似的问题:两个对象相互之间的引用关系/结构化方式是主观的并且会产生实际差异
    • 另外一个优势在于不区分 entity 和 relationship
  • 关系代数中 manipulation 主要包括八个核心操作:restrict, project, product, union, intersection, difference, join, divide
    • 这些操作都具有 closure 性质,也就是运算结果仍然是 relation
  • integrity 主要体现在允许以 declarative 形式指定一组约束
    • 约束类型包括 candidate/primary/foreign keys
  • 数据独立性体现在区分逻辑模型(描述数据的组织和相互关联)和物理存储上
    • 有点类似 essential/accidental 的区分
  • 关系模型并非 Turin complete 所以存在一些增强/扩展

9 Functional relational programming (FRP) #

  • https://aistudio.google.com/prompts/1uwhee1wGIfZnlh2St1XzbrVkb-DgaAFf
  • FRP 是这篇文章提出/提倡的系统架构方法,基本想法是针对 essential logic 使用 FP,针对 essential state 使用关系模型
    • 首要目标是尽可能消除 complexit
  • FRP 的基本架构包括 essential state, essential logic, accidental state/control, other/interface (fig2)
    • essential state 对有状态的组件进行关系式的定义,对应于传统意义上的 state
      • 所有状态都以 relational variable 的形式表达,仅定义其名称和类型(不填充数据)
    • essential logic 包括 derived-relation definition, integrity constraints, pure function,对应于传统意义的行为
      • relational 部分包括一系列由关系代数操作符构建的关系变量的名称和定义
        • 可以借助数据库理论(比如 normalization)指导设计
        • 为每一个 derived relvar 定义一个表达式(由基础的 relation 或者叫作 input data 衍生而来)
      • functional 部分包括系统的本质逻辑
      • 不考虑性能问题
    • accidental state/control 的作用是提升性能
      • 由一系列性能相关的 hint 组成,用于指导 FRP 具体应该如何运行,比如
        • 定义哪些 relational variable 应该被物理存储以提高访问速度
        • 指定状态的物理存储方式
        • 指定 relational variable 应该被即时计算还是 lazily 计算
        • 指定 relational variable 的计算是否应该并行
    • ohter 指的是和用户/外界交互所用的接口
      • 将外部输入转化为关系赋值操作,使输出由 relational variable 的值的变化驱动,二者具体由 feeders 和 observers 执行
        • observer 负责在监控的变量的值发生变化时生成实时的输出
    • 此外还有一些 infra-structure 支持以上组件的运行
  • FRP 的好处主要体现在
    • 不会处于错误的 state(因为 derived state 一般不被存储)
    • 实现了 logic/state 以及 essential/accidental 的分离
    • FP 是完全 referentially transparent 的
    • relational model 使得数据表示不存在主观 bias,并且是 access path independent 的
    • 约束/限制通过声明方式施加
    • 避免了 control flow 或者 concurrency 的问题,同时降低了分布式的难度
    • 避免不必要的数据抽象:高度结构化的数据抽象会导致 referential transparency 被削弱,进一步给 testing/informal reasoning 带来困难

10 Example #

  • 通过一个房地产中介业务的例子展示 FRP 架构的思想
  • 类型包括连续和离散两类
  • essential state 包括六个 base relvar(可以理解为六个 table),分别记录房产、出价、业主对出价的决定、房间信息、楼层、佣金的信息
  • essential logic 包括多个 derived relation 以及多个 function
    • derived relation 分为内部和外部两类
      • 内部包括一些表格(relation)操作得到的派生出的表格(relation),比如房屋面积
        • 用于辅助定义其他 relation 以及约束
      • 外部包括一些用于输出的 relation
  • 约束以 key 的形式给出,比如某个 relation 以某个 attribute 作为 key,以及某个 relation 中的某个离散属性必须在另一个 relation 中存在
    • 另外还有一些 domain-specific constraint,比如数字取值范围的限制
  • 此外还有 feeder 和 observer 作为 interface

Summary #

it may not be FRP, but we believe there can be no doubt that it is simplicity.


Thoughts #

  • 很多地方借鉴/继承了 no silver bullet 这篇文章的想法和术语
  • 作者认为虽然无法解决问题但是不同的现有解决方案还是有高下之分的,比如 informal reasoning 和 testing、OOP/FP
  • 需要思考为什么 von-Neumann 这样的 stateful 架构在计算上是更加 powerful 的
  • 也许 LLM 辅助下 coding 会向 declarative 的方向转变?也许需要一种介于自然语言和高级编程语言之间的语言形式
    • 自然语言仍然是模糊、歧义的,而且规模变大之后层次化、结构化也不是特别好做
  • SQL(作为 layman 认识中和关系模型最接近的对应)确实有 declarative 的特性,用户/开发者只需要清晰描述自己的问题,而不需要考虑任何 execution 层面的事情
  • essential state 其实对于非实时运行的程序(尤其科学计算)不是必要的,所以只需要 essential logic 里面的 FP 部分
    • 也就是说即使从便利性角度出发也没有必要采用 stateful 的方式进行建模
    • 核心的思想是:如果系统不得不引入 state,就以符合关系模型的方式进行引入
  • feeder/observer 的想法可以用于辅导 coding 实践
  • 最后"it may not be FRP" 有点成功不必在我的意味了
  • reference 很少但都是非常有用的

Supplement #

Man-month & no silver bullet #

  • the mythical man-month 出版于 1975,是一本有关软件项目管理的随笔集
    • 最主要的观点是:向已经延期的软件项目增加人力只会让它更加延期
      • 原因在于沟通开销、任务不可分割/并行(人月不能相互替代)、新人 catch-up 需要耗费时间
    • 团队开发出一个相对简洁的系统之后,往往会对第二个系统进行过度设计,导致第二个系统非常臃肿和复杂
    • 软件的设计最好由一个人/小团队来完成,以确保设计/概念的一致性和优雅性
      • 对于软件来说,简洁一致比功能完善更加重要
    • 应该有一个首席程序员负责关键的设计和编码,其他人负责辅助
      • 相比「各自为战」是更好的组织架构
    • 对于全新的系统,第一次设计几乎一定是错误的,应该将其作为可抛弃的原型,积极地构建、学习、抛弃
  • no silver bullet 出版于 1986,主要的观点是:没有任何技术/管理方法可以在十年内让软件生产力/可靠性/简洁性提升一个数量级
    • silver bullet 来源于欧洲民间传说,指唯一一件能够杀死狼人的武器
    • 提出了 essential/accidental complexity 的概念(也被 tar pit 这篇文章引用)
      • essential complexity 指的是业务自身的复杂性,比如需求、数据结构/算法、系统状态的复杂性
      • accidental complexity 指的是由被使用的工具、技术和方法带来的复杂性,尤其是落后的工具(汇编语言等)
      • 这篇文章认为过去几十年的进步(高级语言、集成开发环境)已经消除了 accidental complexity,软件开发剩余的主要是 essential 部分,这部分无法被“神奇地”消除,所以未来软件生产力的提升只能是渐进式的
  • 在 1995 mythical man-month 重新出版的时候,no silver bullet 被作为新增章节收录了进去
  • 类似这三篇文章的其他「软件工程哲学」书籍还包括 design pattern, peopleware, clean architecture, domain-driven design
    • 以及 Rich hickey 2011 的 simple made easy

Software crisis #

  • 第一句话提到的 software crisis 来自 1968 年 NATO 召开的软件工程会议
    • 首次将 software 视作一种 engineering 而不是艺术/个人技巧

von-Neumann architecture #

  • 提出时间是 1940s
  • 特点在于
    • 具有统一的内存空间,程序指令/数据都存储在可读写的内存中
    • CPU 由 ALU (Arithmetic Logic Unit) 和 CU (Control Unit) 组成
      • program counter 负责记录「当前要执行的指令在内存中的地址」
    • 执行过程具体来说是 fetch, decode, execute 的循环
  • 处于运行状态的 von-Neumann 计算机有很多状态变量:内存中的位置存储值、CPU 中寄存器的值、外部设备状态
    • 程序的每一步都实现了系统/计算机状态的一次修改
  • von-Neumann 架构之外的 alternative 中,最出名的是 Harvard architecture,核心区别在于将数据和指令分开存储
    • 优势在于指令内存不可修改所以更安全、CPU 可以同时执行取指令以及读写数据的操作(突破 von-Neumann bottleneck)
    • 比较广泛地用于嵌入式系统和数字信号处理器中
    • x86 和 ARM 实际上是两种架构的混合:CPU 内部有 cache 并且 I/D cache 是分开的
  • state machine 是一个描述系统行为的数学模型,定义了「一个系统面对不同事件时如何在状态之间转换」
    • 状态是系统处在的所有不同情况的有限集合,在不同事件/输入的触发下系统在状态之间进行转移
    • 可以模拟现实中大部分有行为的系统

Relational model #

  • 由 Edgar Codd 于 1970 提出
  • 关系数据库设计的核心理论是 normalization,包括 1NF, 2NF, 3NF, BCNF
  • manipulation 除了以上八条之外还包括 outer join 等,而 divide 操作实际上很少被用到
  • 这篇文章仅考虑 algebra,而关系模型的另一部分 calculus 没有提到
  • 关系模型中 NULL value 引起了很多讨论,Chris Date 等人主张废除 NULL
    • 关系模型没有很好地给出对 NULL 的描述,所以实际上存在复杂的三值逻辑(NULL=NULL 的结果是 UNKNOWN)
  • SQL 和关系模型之间的差异主要体现在
    • SQL table 是 multiset,允许重复的行存在
    • SQL 具有顺序的概念,尤其是 ORDER BY 语法
    • SQL 允许重复列名的存在
  • SQL 的语法关键词采用大写字母的原因是早期计算环境和显示条件限制下,需要将语言结构性部分和数据内容部分在视觉上区分开来,类似一种语法高亮
    • 但是并不严格限制大小写,select/SeLeCt 也同样 work
  • access path independent 指的是只需要声明数据,不需要指定系统如何找到数据
  • relvar (relational variable) 指的是值为 relation 的变量,在不同时间具有不同的 table/relation 形式的值