不用任何賦值的程式設計稱為*函式式*程式設計

方應杭在飢人谷發表於2018-12-03

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.im/post/5c02ca…

相關文章