# 函数式编程

个人理解的函数式编程是函数或属性可以作为另外函数的返回值,并在其他函数中使用,可以一个函数一个函数互相调用,形成的蜘蛛网似的结构。函数式编程旨在尽可能地提高代码的无状态性和不变形

  • 面向对象编程(OO)通过封装变化使得代码更易理解
  • 函数式编程(FP)通过最小化变化使得代码更易理解

当我们考虑应用设计时,问问自己是否遵从了以下的设计原则

  • 可拓展性:我是否需要不断的重构代码来支持额外的功能
  • 易模块化:如果我更改了一个文件,另一个文件会不会受到影响
  • 可重用性:是否有很多重复的代码?
  • 可测性:给这些函数添加单元测试是否让我纠结?
  • 易推理性:我写的代码是否非结构化严重并难以推理?

函数式编程所基于的一些基本概念:

  • 声明式编程
  • 纯函数
  • 引用透明
  • 不可变性

# 纯函数

函数式编程基于一个前提,即使用纯函数构建具有不变性的程序。纯函数具有以下性质:

  • 仅取决于提供的输入,而不依赖与任何在函数求职期间或调用间隔可能变化的隐藏状态和外部状态
  • 不会造成超出其作用域的变化,例如修改全局对象或引用传递的参数

任何不符合上述条件的函数都是不纯的,比如

var counter = 0
function increment() {
  return ++counter
}

因为其读取并修改了一个外部变量,即函数作用与外的counter。一般来讲,函数在读取或写入外部资源时都会产生副作用。以下操作都有可能造成副作用

  • 改变一个全局的变量、属性或数据结构
  • 改变一个函数参数的原始值
  • 处理用户输入
  • 抛出一个异常,除非它又被当前函数捕获了
  • 屏幕打印或记录日志
  • 查询HTML文档、浏览器的cookie或访问数据库

一段有副作用的函数,也是我们经常写的一段函数

function showStudent(ssn) {
  var student = db.get(ssn)
  if (student !== null) {
    document.querySelector(`#{elementId}`).innerHTML = `${student.ssn}, ${student.firstname}, ${student.lastname}`
  } else {
    throw new Error('Student not found!')
  }
}
showStudent('888-88-888')

分析代码,这个函数会有一些副作用暴露到其作用域外:

  • 该函数为访问数据,与一个外部变量(db)进行了交互,因为该函数签名中并没有声明该参数。在任何一个时间点,这个引用可能为null,或在调用间隔改变,从而导致完全不同的结果并破坏了程序的完整性。因为不确定外部环境对db做了什么修改,可能会直接修改db.get = null,从而造成函数内部出现异常。
  • 全局变量 elementId可能随时改变,难以控制。还是对全局变量引用时的副作用
  • HTML元素被直接修改了。HTML文档(DOM)本身是一个可变的,共享的全局资源。还是对全局变量引用时的副作用,但是哪怕抽离成单独的函数体,这个也是一个不可避免的副作用,不过我们本质是尽可能减少函数的副作用,而不是没有副作用
  • 如果没有找到学生,该函数会抛出一个异常,这将导致整个程序的栈回退并突然结束。所以应该将有异常处理的逻辑单独抽离为一个函数体,不让其影响函数体内其他逻辑的运行

我们通过函数柯里化修改函数

  • 通过抽离函数,将一个函数改变成多个具有单一职责的短函数。
  • 通过显式地将完成功能所需的依赖都定义为函数参数来减少副作用的数量 比如以下代码,使用curry函数减少findappend函数的参数,使其成为可以与run组合的一元函数
var find = curry(function(db, id) {
  var obj = db.get(id)
  if (obj === null) {
    throw new Error('Object not found!')
  }
  return obj
})
var csv = (student) => {
  return `${student.ssn}, ${student.firstname}, ${student.lastname}`
}
var append = curry(function(elementId, info) {
  document.querySelector(elementId).innerHTML = info
})

// 函数柯里化,通过减少这些函数的长度,将showStudent编写为这些小涵书的组合
var showStudent = run(
  append('#student-info'),
  csv,
  find(db)
)

showStudent('888-88-888')

这样的代码修改带来的优势:

  • 灵活了很多,因为现在有三个可以被重用的组件。
  • 这种细粒度函数的重用是提供工作效率的一种手段,因为你可以大大减少需要主动维护的代码量。
  • 声明式的代码风格提供了程序需要执行的那些高阶步骤的一个清晰视图,增强了代码可读性
  • 更重要的是,与HTML对象的交互被移动到一个单独的函数中,将纯函数从不纯的行为中分离出来。柯里化以及如何管理纯与不纯的代码将在后续进一步解释。

但是这个程序仍然有一些问题需要解决,但减少副作用能够在修改外部条件时使程序不那么脆弱。如果仔细看一下find函数,就会发现它有一个可以产生异常的检查null值的分支,我们需要明白,确保一个函数有相同返回值是一个优点,它是的函数的结果是一致的和可预测的这是纯函数的一个特质,叫函数透明