OOP&FP

Comparison #

https://aistudio.google.com/prompts/1LDnC18lR-EzVTPtsSlqCDdE8Ll80eK1o

OOP #

  • 特征是
    • 对象的状态是可变的
    • 用封装、继承、多态来组织代码
      • 封装将数据和对数据的操作捆绑到一起,保证了低耦合、高内聚
      • 继承保证代码可复用性
    • 对象属性的可变性导致并发非常困难,一般需要 lock
  • 优势在于
    • 建模非常直观,贴近现代世界的构成原理,尤其是对于大量长期存在、状态可变的复杂实体
    • 对象是具有状态、可变的
  • 劣势在于
    • You wanted a banana, but what you got was a gorilla holding the banana and the entire jungle.
      • 这是继承的滥用问题导致的
      • arguement:香蕉不会独立出现,仅会出现在某个地理区域的某个动物手上/某棵树上
    • 当系统变得庞大时,追踪和管理无数对象的状态会变得非常困难,存在意料之外的副作用
    • 实现单个简单功能需要预先写大量的模板代码
  • 适用的场景:可以被很好抽象和建模的系统(尤其是 GUI 和游戏)、系统内包括很多具有独立生命周期和状态的实体
  • 成熟的 OOP 框架包括 Java Spring、C#、Python Django

FP #

  • 优势在于
    • 纯函数没有副作用,给定相同的输入,永远返回相同的输出。这使得代码单元的行为完全独立,极大地降低了系统复杂性。你修改一个函数,不必担心它会意外地改变系统的某个遥远角落的状态。
      • 「没有 side effect」的意思是大部分时候函数除了返回之外不做其他事,函数只做自身的事情
    • 天然有利于单元测试和并发
    • 天然有利于构建可复用的函数以及自然的模块化、进一步地也有利于大型系统的构建
    • 可变状态和 side effect 是两个 bug 的主要来源,所以 FP 程序更加 robust
  • 劣势在于
    • 数据不可变可能带来性能负担
    • 在 IO 操作、网络等方面需要额外努力
  • 适用的场景:数据处理和分析、高并发的后端服务,尤其是问题可以描述为一个输入 -> 输出的数据流

Languages #

  • 以 FP 为主的语言包括:Haskell(纯粹的函数)、Lisp、Js/Ts、RUST
    • Kotlin 相比 Python 在 FP/OOP 方面都更加强大、深入
    • RUST 更加偏向 FP,没有类和继承,而是用组合来复用代码
    • GO 在两个方面都非常简单,主要的目的是并发
  • RUST 对 OOP/FP 的处理
    • 采纳了 OOP 中的封装范式(通过 struct/impl)
    • 没有实现 OOP 的 class 以及继承,因为
      • 父类和子类的实现是高度耦合的
      • banana/gorilla/jungle
      • 设计/维护父类的时候无法考虑到子类的全部使用场景
    • 通过 trait 实现多态,只要一个类型实现了 trait 中指定的方法就可以认为是 trait 的一种
      • 受到了“组合优于继承”的原则指导
      • 优点在于更加灵活、安全和松耦合
    • 在 RUST 中变量默认是 immutable 的,主要出于增强安全性的考虑
    • 此外 RUST 的 ownership 是全新的实践,强化了封装并且杜绝了数据之间的竞争

Summary #

  • 总之:如果 coding 面对的系统不是由天然的具有生命周期和状态的对象组成、描述对象比描述函数更不直观,那么就应该使用 FP
  • 两种范式的结合一般是:OOP 用来建构系统的骨架,而 FP 用来实现数据处理的逻辑
  • FP 的工具:高阶函数(Higher-Order Functions)、柯里化(Currying)、函数组合(Composition)
    • 高阶函数指的是函数的函数,在 Python 中表现为 decorators 以及 map/filter 这样的内置函数
    • currying 指的是固定函数中的一些参数形成一个新函数,Python 中的应用是 functools.partial,有点类似 OOP 中的继承
    • 组合的意思是将前一个函数的输出串联到后一个函数的输入上,可以用 reduce 帮助实现,或者 toolz 这样的第三方库
  • toolz 和 PyMonad 极大地增强了 Python 中 FP 的功能
  • monad 和 functor 的术语源自 category theory
    • functor 必须提供一个 map 方法(以函数为参数),将「盒子」中的值传递给作为作为参数的函数,然后将返回值包装在同样的「盒子」里返回
    • 在 functor 的基础上,monad 能够保证返回值仅包含一层「盒子」,也就是如果函数计算的结果就是一个「盒子」就不要再加一层「盒子」
    • 类似 Maybe 这样的 monads 可以在比较优雅地处理错误,不会抛出 exception

FP in Python #

  • https://aistudio.google.com/prompts/1V8CnIDpFlzHVfqrgdzfKVb5vC1TK72tz
  • 函数和其他数据类型没有本质区别,可以赋值、作为参数传递、作为返回值
  • 接收其他函数作为参数的函数称作高阶函数
    • map 和 filter 可以被列表推导式/生成器表达式代替
      • map(funciton, iterable) 将 function 作用于 iterable 中的每一个元素(Mathematica 中也类似)
      • filter(funciton, iterable) 筛选 iterable 中经过 function 处理返回值为 True 的元素
    • reduce 对元素进行累积计算
    • sorted 可以接收函数作为排序规则
  • 尽量追求 immutability
    • Python 中 tuple, str, int, float 是不可变的,可变的主要是 list/dict
    • 不使用 list append 或者 dict 直接增加 kv 的操作,而是用一个新的 list/dict 替换
  • 避免副作用:纯函数不应该修改任何全局变量或者传入的参数,也不应该进行 IO 操作
  • 标准库 itertools 提供了创建迭代器的工具,functools 提供了高阶函数工具
    • toolz 提供了很多 FP 的工具,比如 pipe
  • 一个项目应该分为 functional core 和 imperative shell
    • core 包含核心逻辑、不关心合适何地进行这些操作
    • shell 负责读取文件、IO、执行 core 函数
  • 按照 verb 而不是 noun 来组织文件
  • 多写小函数,然后把小函数组合起来
  • 切分函数的判断准则
    • 函数实现的功能是否可以简单地总结为一句话(函数是否有一个简洁且名副其实的名字)
    • 代码是否会被复用
    • 是否需要独立的 unit test