不用任何赋值的程序设计称为*函数式*程序设计

875 阅读4分钟

SICP 第三章的标题是:模块化、对象和状态。我在这一章找到了「函数式程序设计」的定义(见知识点五),哈哈真是一本神书。

知识点一:

开篇的一段话十分吸引我,这段话在高层次上说明了面向对象编程的缺点,以及 Rx.js 这种编程范式的优点。

有一种非常强有力的设计策略,特别适合于构造那些模拟真实物理系统的程序,那就是「基于被模拟的系统的结构去设计程序的结构」。

在这一章里,我们要研究两种特点很鲜明的组织策略,它们源自对于系统结构的两种非常不同的世界观。第一种策略将注意力集中在「对象」身上,将一个大型系统看成一大批对象,他们的行为可能随着时间的进展而不断变化。第二种策略将注意力集中在流过系统的信息流上,非常像电子工程师观察一个信号处理系统。

基于对象的途径和基于流处理的途径,都对程序设计提出了具有重要意义的语言要求。

对于对象途径而言,我们必须关注计算对象可以怎样变化而又同时保持其标识。这将迫使我们抛弃老的计算的代换模型,转向更机械式的、理论上也更不容易把握的环境模型。在处理对象、变化和标识时,各种困难的根源都在于我们需要在这一计算模型中与时间搏斗。如果允许程序并发执行的话,事情就会变得更困难。

对于流方式来说,它特别能够用于松解在我们的模型中对时间的模拟和计算机求值过程中的各种事件的发生顺序。我们将通过延时求值做到这一点。

知识点二:对象是有状态的

考虑一个取钱的函数 withdraw

// 初始金额 100
> withdraw(25) 
< 75 // 余额 75
> withdraw(25) 
< 50
> withdraw(25) 
< Error: 余额不足
> withdraw(15)
< 35

同样一个函数,每次执行的结果却不一样。第一章的代换模型不再有用了。

withdraw 应该如何用「过程」实现呢?记得吗,之前我们说过也许数据结构都可以用「过程」实现。

let withdraw = (() => {
  let balance = 100
  return (amount) => {
    if(amount <= balance){
      balance = balance - amount
      return balance
    }else{
      throw new Error('余额不足')
    }
  }
})()

这一句 balance = balance - amount 是本书首次出现的对一个量进行赋值的语句(这里 let balance = 100 是「初始化」不是「赋值」, balance 的第二次赋值才是「赋值」)。

Scheme 语言里赋值的语法是

set! balance (- balance amount)

赋值被设计成 set! ,足见 Scheme 对赋值的厌恶。

知识点三:两个对象的状态是互相独立的

我们用 makeWithdraw 来创建两个 withdraw,会发现它们两个的状态是互不相干的:

let makeWithdraw = (balance) => (amount) => {
  if(amount < balance){
    balance = balance - amount
    return balance
  }else{
    throw new Error('余额不足')
  }
}

下面是两个 withdraw 的行为:

let withdraw1 = makeWithdraw(100)
let withdraw2 = makeWithdraw(100)
withdraw1(50) // 50
withdraw2(70) // 30

知识点四:消息传递风格

使用消息传递风格就可以构造 account 对象了,account 对象可以响应 withdraw(取钱)和 deposit(存钱)消息:

let makeAccount = balance => {
  let withdraw = (amount) => {
    if(amount < balance){
      balance = balance - amount
      return balance
    }else{
      throw new Error('余额不足')
    }
  }
  let deposit = (amount) => {
    balance = balance + amount
    return balance
  }
  let dispatch = (m) => {
    return (
    m === 'withdraw' ? withdraw :
    m === 'deposit' ? deposit :
    new Error('unknown request'))
  }
  return dispatch
}

接下来是使用 makeAccount 创造两个 account 对象(其实是过程):

let account1 = makeAccount(100)
account1('withdraw')(70) // 30
account1('deposit')(50) // 80
写成 Scheme 其实更像是消息传递
((account1 'withdraw) 50)
((account1 'deposit ) 50)

知识点五:赋值的利弊

将赋值引进程序设计语言,将会使我们陷入许多困难概念的丛林中。

但是它就没有好处吗?

与所有状态都必须现实地操作和传递额外参数的方式相比,通过引进赋值以及将状态隐藏在局部变量中的技术,能让我们以一种更___模块化___的方式构造系统。

但是这本书马上又加了一句话:

可惜的是,我们很快就会发现,事情并不是这么简单。

接下来就进入了「赋值的代价」小节:

赋值操作使我们可以去模拟带有局部状态的对象,但是,这是有代价的,它是我们的程序设计语言不能再用代换模型来解释了。进一步说,任何具有「漂亮」数学性质的简单模型,都不可能继续适合作为处理对象和赋值的框架了。

只要我们不使用赋值,一个「过程」接收同样的参数一定会产生同样的结果,因此就可以认为这个「过程」是在计算「数学函数」。

不用任何赋值的程序设计称为函数式程序设计。

未完待续。

第二章的笔记:juejin.cn/post/684490…