一篇有關函數語言程式設計的形象生動教程

banq發表於2018-11-12

函數語言程式設計(FP)與物件導向程式設計(OOP)的誕生的時間差不多,但它最近才最受歡迎,特別是在JavaScript社群中,為什麼?

我在00年代早期就學麻省理工學院。計算機程式的體系結構和解釋(SICP)是我的教科書。所以我的第一個正式學習的程式語言是函式性的,然後我在工業界工作了十多年,幾乎沒有多少時間考慮過使用FP。當得知大學的教科書被認為是“函式性程式設計聖經”時很震驚。

別誤會我的意思,這是一本很好的教科書。我敢肯定它會讓我成為一個更好的程式設計師,但這並不是我需要經常在Java / ActionScript / PHP / Python / Ruby / JavaScript等職業生涯中使用的東西。

然後我在Wyncode Academy任教四年,並發現自己試圖向新手解釋FP概念。這很難 - 比OOP更難。

為什麼FP比OOP更難?

相關問題:為什麼FP需要這麼長時間才能流行起來?

我們編碼社群需要解決為什麼FP難以教授。像一個宗教說教一樣傳播FP重複了同樣的錯誤,導致FP長期在這個行業中萎靡不振。

對FP的許多介紹都遺漏了一些東西。它不僅僅是一種替代程式設計風格。這是一種新的思維方式,它代表的類似的技巧也可以與來自OOP背景的更有經驗的程式設計師一起使用。

我在Wyncode中使用的技術之一就是講述一個難以理解的概念時,首先讓學生了解概念的背景以及 這個概念的大致樣子(big picture),之後就更容易解釋技術細節了。

大致樣子(big picture):歷史背景
計算機是如何工作的?最常見的(流行的?易於理解的?)計算模型是圖靈機。FP程式設計師抱怨的狀態正在圖靈機中正視著我們。用於操作該機器的演算法表示不同狀態之間的轉換,例如從開啟 / 關閉 (1或0)的一些盒子到開啟 / 關閉的一些其他盒子。
如果我們試圖想象兩個圖靈機同時在同一磁帶上執行,我們就可以開始理解為什麼“共享狀態”和OOP中的併發性都是難題。
圖靈機是一臺通用機器。它可用於解決每個可有效計算的數學和邏輯問題。這是個簡單的操作集: 向左移動,向右移動,寫點,讀點,擦除點(增刪改查CRUD)。 足以(給予足夠的時間和資源)來解決宇宙中的每個數學問題。
這就是艾倫·圖靈在1936年所證明的。

在許多方面,圖靈機是計算機“工作”的方式。但這也是計算機的工作原理。但是還有另外一種理論:

電路是不同的計算模型。每個邏輯閘(AND,OR,NAND,NOR,XOR等)都是純函式。他們接受輸入併產生沒有副作用的輸出。如果我們只有能夠建立和組合這些“函式”,我們還可以解決宇宙中每個可解決的數學問題。這也是阿隆佐·邱奇在1936年證明的。

所以我們有兩種不同的計算模型:圖靈機​​的0和1(物件)和阿隆佐·邱奇用邏輯閘(函式)構建的lambda演算。哪一個是正確的?

有一段時間,關於抽象圖靈機是否能解決與lambda演算相同的數學問題(反之亦然)的辯論。最終他們被證明是等同的。

等同意味著它們同樣強大。任何可以為圖靈機編寫的演算法也可以使用函式編寫。因此,任何可以用圖靈機軟體編寫的程式也可以用電路硬體表示。

“硬體程式設計”是什麼意思?我們可以看到專用積體電路(ASIC)中體現的“硬體程式設計” 。可以建立“程式設計”的電路,以便快速完成一件事,比如我的比特幣下棋

我們現在有兩種程式設計選擇。硬體更快,軟體更慢。在軟體中犯了錯誤?只需點選刪除鍵,然後重試。硬體出錯?是時候抓烙鐵了。這是一個經典的工程設計權衡。

因此,假設我們有一個以OOP風格編寫的演算法,我們希望將其轉換為ASIC硬體程式設計。以FP風格重寫程式可能是一個很好的策略,因此它可以更好地對映到底層電路圖。

面向FP的語言往往看起來像電路。特別是Unix,Elixir,F#,JavaScript和其他“管道操作員” 使程式碼看起來像電路圖:輸入進入左側,流過許多“門”(管道)直到它們被轉換進入右邊的最終輸出。某些語言(|>)使用的管道運算子看起來像邏輯閘,這可能不是巧合。


大致樣子(big picture):哲學
我拿到了我的CS學位的哲學輔修,所以我著迷的一件事是這兩個研究領域的交集。我發現在教授新程式設計師時,特別是那些具有人文學科而不是STEM背景的程式設計師,有助於在這兩個領域重疊處討論思想。

FP中一個哲學上重要的概念是“函式相等 functionally equivalent”。

也許證明這種等價性的最好例子是湯姆斯圖爾特的偉大文章“從無開始程式設計”。這裡借用他對數字如何完全用函式表示的方式來解釋編碼:

首先將數字0的概念定義為接受函式引數但不對其執行任何操作的函式。

# Ruby
ZERO = -> (func) { 
  # does nothing
  func
}


類似地,我們可以將所有自然數定義為接受函式引數的函式,並將它們呼叫n次。

ONE = -> (func) {
  # calls it once
  # same as func.call()
  func[];
  func
}

TWO = -> (func) {
  # calls it twice
  func[]
  func[]
  func
}


要測試這些“函式數字”,請將它們傳遞給測試函式。

HELLO = ->() { puts "hello" }

# same as: ZERO.call(HELLO)
ZERO[HELLO] # nothing displayed
ONE[HELLO]  # one "hello" displayed
TWO[HELLO]  # "hello" twice


這種函式數字符號表示法其實很難玩和除錯。因此,為了更容易使用,我們可以定義一個函式,將這些函式數轉換為我們習慣使用的物件數字。

# convert number function into number object
def to_integer(func)
  # count how many times counter is called
  n = 0
  counter = ->() { n += 1 }
  func[counter]
  n
end

p to_integer(ZERO) # 0
p to_integer(ONE)  # 1
p to_integer(TWO)  # 2


這個轉換器建立計數功能並將其傳遞給數字函式。ZERO函式將呼叫它為零次,ONE函式將呼叫它一次,等等。我們跟蹤計數器被呼叫多少次以獲得結果。

對於這些函式數字定義,我們可以實現新增各種功能:ADD SUM統計等。

ADD = -> (func1, func2) {
  -> (f) { func1[func2[f]] }
}

sum = ADD[ZERO, ZERO]
p to_integer(sum) # 0

sum = ADD[ZERO, ONE]
p to_integer(sum) # 1

sum = ADD[ONE, ONE]
p to_integer(sum) # 2



如果TWO呼叫一個函式兩次,那麼ADD[TWO, TWO]將返回一個呼叫其引數四次的函式數字(函式數字FOUR)。

這是一個令人費解的方式。當我看完“從無開始程式設計”這本書時,我感覺這是一個巧妙應用基本電腦科學概念的有趣書籍,但不是我在日常工作中可以使用的東西。

而這正是我(我懷疑很多其他人)對FP一般的感覺 - 它很聰明,但似乎沒有用。這是我們需要解決的問題。

所以,教FP更好的開始地方是“駭客帝國3:矩陣革命”。

什麼是矩陣與FP呢? 這部電影告訴我們:我們沒有證據證明我們周圍的世界是真實的。也許世界上有實際的物體,或者我們只是在罐子裡的大腦

因此,至少有兩個相互矛盾的理論,例如,第一個是什麼。我們可以與之互動(觸控和感覺)是一種(名詞,物件)嗎?或者它是一個動作(一個動詞,一個函式),一個作用於世界的東西,但沒有實在的呈現?

函式數字one是數字1的模擬,它是在函式等同 functionally equivalent於物件數字one,意味著它可以做物件數字one做的任何事情。

但是OOP中的物件“存在”並不是真的“存在”。這是一個矩陣模擬。它沒有固有屬性 - 它不是x,它只是像x。

打個比喻,你坐在真正的椅子上只是一種用力按壓你的身體功能?“椅子”可以是存在於現實世界中的椅子這個物件,也可以是一種椅子功能:一種希望用舒適的力量推動你的功能,並沒有潛在的客觀基礎。

想想顏色,紅色美味的蘋果真的是紅色的(形容詞、描述名詞)還是扮演紅色(動詞)?顏色是真正的底層蘋果物件的固有屬性,還是僅僅是當光照在它上面時執行函式的動作結果呢?蘋果真的還是模擬的呢?

解釋這個哲學概念的難度是解釋為什麼FP難以教授的好隱喻。為了幫助學生理解,首先要開放他們的想法,讓世界完全由“功能/函式”組成。從樣子big picture概念開始,然後轉向世界的FP模型:它們與OOP表示的區別以及它們如何具有相同的結果。(banq注:從概念開始轉向有兩個:一個是名詞,一個是動詞,FP是轉向到動詞,而我們日常是轉向名詞,但是很多人沒有意識到這種轉向,所以很難轉到動詞方向去接受函式程式設計)

 

相關文章