如何利用Ruby簡單模擬Lambda演算詳解
最近看一本叫做《計(jì)算的本質(zhì)》的書,這本書主要說了一些底層計(jì)算方面的知識(shí)??梢哉f它刷新了我的三觀,而當(dāng)今天看到可以使用Y組合子來實(shí)現(xiàn)遞歸的時(shí)候我的世界觀基本崩塌了。故借著七夕來寫一篇文章總結(jié)一些關(guān)于計(jì)算的一些基本認(rèn)識(shí)。以便后續(xù)可以更好地學(xué)習(xí)。也借著Ruby的語法來闡述一下關(guān)于Lambda的一些故事。
0. 題外話
為了慶祝一下這個(gè)七夕節(jié)日,我提前關(guān)掉了LOL,打開了Emacs,敲下如下代碼(這里順便推廣一下Ruby的單件方法)
subject = "情侶" object = "狗" def subject.do_something(who) "#{self} 虐 #{who}" end if __FILE__ == $0 p subject.do_something(object) p object.do_something(subject) end
上面代碼的運(yùn)行結(jié)果是
"情侶 虐 狗" dog.rb:11:in `<main>': undefined method `do_something' for "狗":String (NoMethodError)
很明顯,情侶可以“虐”狗但狗不能“虐”情侶。因此第二句執(zhí)行語句會(huì)報(bào)錯(cuò)。以上也是Ruby優(yōu)雅的地方,我可以直接在指定實(shí)例上定義方法,而不影響其他其他的同類的實(shí)例(以上實(shí)例都是字符串)。
1. 函數(shù)的一些基本認(rèn)識(shí)
“題外話”有個(gè)卵子用?額, 說沒用,它還是有一點(diǎn)作用的。我們今天的主題是用Ruby來模擬Lambda演算。Lambda演算在Wiki上面的解釋是這樣的
Lambda演算可以被稱為最小的通用程序設(shè)計(jì)語言。它包括一條變換規(guī)則(變量替換)和一條函數(shù)定義方式,Lambda演算之通用在于,任何一個(gè)可計(jì)算函數(shù)都能用這種形式來表達(dá)和求值。
平時(shí)我們使用命令式的編程語言會(huì)更傾向于關(guān)注字符串, 數(shù)字,布爾 這些可以充當(dāng)主語或者賓語的類型。而我們平時(shí)跟他們打交道更多會(huì)以變量的形式,就如同“題外話”中的"狗"和"情侶"。但這篇文章的重點(diǎn)放在"虐"這個(gè)詞上,也就是我們常稱的謂語。在計(jì)算機(jī)里面我們通常稱他做方法 或者 函數(shù)。
既然Wiki上也說了Lambda是最小的通用程序設(shè)計(jì)語言,那我們有沒有可能用Lambda來模擬出數(shù)字, 字符串, 布爾等等的這些常用的數(shù)據(jù)類型呢?這就是接下來要講的東西。
1) Ruby中的函數(shù)
在Ruby中,函數(shù)其實(shí)可以算是一等公民,只是它的鋒芒往往被Ruby強(qiáng)大的面向?qū)ο筇卣鹘o掩蓋掉了(它使得我們更多地關(guān)注類還有模塊)。Ruby里面有個(gè)十分簡單的創(chuàng)建函數(shù)的方式
[1] pry(main)> -> x { x + 2 } => #<Proc:0x007fc171dc6010@(pry):1 (lambda)>
它返回了一個(gè)Proc對(duì)象。其實(shí)這個(gè)對(duì)象,就類似于我們平時(shí)操作的函數(shù)對(duì)象。但是這里我們并沒有給函數(shù)賦予名字,可以理解為它是一個(gè)匿名函數(shù)。那么這種函數(shù)如何調(diào)用呢?有一種很語義化的調(diào)用方式,我們甚至不需要用變量來接受這個(gè)函數(shù)就可以調(diào)用它。
[2] pry(main)> -> x { x + 2 }.call(1000) => 1002 [3] pry(main)> -> x { x + 2 }.call(1000, 100000) ArgumentError: wrong number of arguments (given 2, expected 1) from (pry):3:in `block in __pry__'
Ruby還提供了參數(shù)檢測,如果傳入的參數(shù)與定義該函數(shù)的時(shí)候不匹配的話,則會(huì)拋出ArgumentError異常。此外,Ruby還提供了一種語法糖,我們可以用Proc#[]
包裹參數(shù)來調(diào)用Proc實(shí)例。
使用方式如下:
[4] pry(main)> ADD_THREE = -> x { x + 3 } => #<Proc:0x007fd8341ffc48@(pry):4 (lambda)> [5] pry(main)> ADD_THREE[1000] => 1003
2) 柯里化
Wiki 上的解釋如下
在計(jì)算機(jī)科學(xué)中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
既然上面已經(jīng)講了Ruby創(chuàng)建匿名函數(shù)的基本方式,那下面來看看如何用Ruby來模擬一個(gè)柯里化的過程, 假設(shè)我有一個(gè)函數(shù)接收三個(gè)參數(shù), 我定義如下
[10] pry(main)> ADD_THREE_NUMBER = -> (x, y, z) { x + y + z} => #<Proc:0x007fd834aa4150@(pry):10 (lambda)> [11] pry(main)> ADD_THREE_NUMBER.call(1,2,3) => 6
PS: 定義函數(shù)時(shí)參數(shù)用括號(hào)包裹是為了提高識(shí)別度,其實(shí)括號(hào)可加可不加,定義函數(shù)也可以寫成 -> x, y, z { x + y + z }
上述函數(shù)如何柯里化?按照柯里化的定義,我們可以把多參數(shù)的函數(shù)寫成嵌套返回多個(gè)單一參數(shù)函數(shù)的形式,為了方便閱讀我把代碼寫在Ruby腳本文件里面
# currying.rb ADD_THREE_NUMBER_NEW = -> (a) { -> (b) { -> (c) { a + b + c } } } if __FILE__ == $0 p ADD_THREE_NUMBER_NEW.call(1).call(2).call(3) end
運(yùn)行結(jié)果
6
其實(shí)這個(gè)函數(shù)每次調(diào)用都返回了一個(gè)只帶一個(gè)參數(shù)的函數(shù),而且返回的函數(shù)會(huì)保存著調(diào)用當(dāng)前函數(shù)時(shí)傳入的參數(shù),就是我們通常講的閉包。直到最后一個(gè)函數(shù)被調(diào)用并返回的時(shí)候,才會(huì)真正得到期望的計(jì)算結(jié)果。
當(dāng)然我們可以更簡單的用Proc#[]來調(diào)用(在文章的后半部分我會(huì)統(tǒng)一用這種方式,比較節(jié)省代碼)
[1] pry(main)> require('./currying') => true [2] pry(main)> ADD_THREE_NUMBER_NEW[1][2][3] => 6
2. 模擬Lambda演算
Lambda既然是最小的通用編程語言,那么我們現(xiàn)在嘗試一下用Ruby的Proc這個(gè)現(xiàn)成的Lambda來演算一些東西。難的東西我自己都還接受不了,這里只能先來模擬一些最為簡單的東西了。
1) 數(shù)字
首先嘗試模擬一下數(shù)字。《計(jì)算的本質(zhì)》一書中提供了一個(gè)比較直觀的段子,以下是我概括的大意
我們?nèi)绻麤]辦法直接使用數(shù)字,而只能使用謂語(動(dòng)作),那么我們只能重復(fù)數(shù)這個(gè)動(dòng)作來描述數(shù)字這個(gè)特征,而數(shù)這個(gè)動(dòng)作其實(shí)就是我們需要寫的Lambda表達(dá)式
直觀點(diǎn)講當(dāng)我們要表示0的時(shí)候就數(shù)0次,調(diào)用方法0次,表示1的時(shí)候就調(diào)用方法一次。
那我們簡單地表示0~2就可以是
ZERO = -> (p) { -> (x) { x } } ONE = -> (p) { -> (x) { p[x] } } TWO = -> (p) { -> (x) { p[p[x]] } }
這樣或許看起來有點(diǎn)迷糊,其實(shí)他們都用Lambda演算出來的,他們都接受一個(gè)函數(shù)p(數(shù)數(shù)這個(gè)動(dòng)作)以及一個(gè)基礎(chǔ)值x作為參數(shù),如果是ZERO就直接返回基礎(chǔ)值x, 如果是ONE就以x這個(gè)基礎(chǔ)值作為參數(shù)調(diào)用函數(shù)p表示數(shù)了一次。
這里我們并沒有辦法很好的表示這個(gè)基礎(chǔ)值x,為了直觀,我需要借用一下Ruby內(nèi)置的數(shù)字0 作為一個(gè)基礎(chǔ)值,并且要另外定義數(shù)數(shù)這個(gè)動(dòng)作。
CALCULATE = -> (n) { n + 1 }
其實(shí)數(shù)數(shù)的動(dòng)作就是在原來的基礎(chǔ)值上加1,最后我統(tǒng)一寫腳本
# coding: utf-8 # number.rb ZERO = -> (p) { -> (x) { x } } ONE = -> (p) { -> (x) { p[x] } } TWO = -> (p) { -> (x) { p[p[x]] } } def to_integer(proc) calculate = -> (n) { n + 1 } # 其中0是基礎(chǔ)值 proc[calculate][0] end
在解析環(huán)境里面引入腳本并執(zhí)行一些相關(guān)的語句,就能得到我們想要的結(jié)果了
[1] pry(main)> require('./number') => true [2] pry(main)> to_integer(ZERO) => 0 [3] pry(main)> to_integer(ONE) => 1 [4] pry(main)> to_integer(TWO) => 2
雖然對(duì)于已經(jīng)含有內(nèi)置數(shù)字類型的Ruby來說這種模擬完全沒有任何實(shí)用價(jià)值,不過對(duì)于了解Lambda演算這可以是一個(gè)不錯(cuò)的開始。
2) 布爾型
說完數(shù)字,再來簡單說一下布爾類型吧,他們也算是比較基礎(chǔ)的數(shù)據(jù)類型了。而且布爾型模擬起來還更簡單些。畢竟布爾型,不是true就是false。我們可以分別寫兩個(gè)都接受兩個(gè)參數(shù)的函數(shù),一個(gè)代表true一個(gè)代表false。true函數(shù)就返回其中的第一個(gè)參數(shù),false函數(shù)直接返回第二個(gè)參數(shù)。
TRUE = -> (x) { -> (y) { x }} FALSE = -> (x) { -> (y) { y }}
我們?cè)賹懸粋€(gè)解析腳本,作為驗(yàn)證。我記得在C這種沒有布爾類型的語言中我們是用0代表false 用大于1代表true。這里我就簡單用0和1作為基礎(chǔ)值來驗(yàn)證我們的Lambda演算是否正確
# boolean.rb TRUE = -> (x) { -> (y) { x }} FALSE = -> (x) { -> (y) { y }} def to_boolean(proc) proc[1][0] end
引入運(yùn)行腳本試試
[1] pry(main)> require('./boolean') /Users/lan/Personal/Ruby/boolean.rb:1: warning: already initialized constant TRUE /Users/lan/Personal/Ruby/boolean.rb:2: warning: already initialized constant FALSE => true [2] pry(main)> to_boolean(FALSE) => 0 [3] pry(main)> to_boolean(TRUE) => 1
跟預(yù)期的一樣,我們的模擬是正確的,F(xiàn)ALSE函數(shù)最后被解析成0, 而TRUE函數(shù)最后被解析成1。
以上的警告是重復(fù)定義常量所致,這里可以暫時(shí)忽略。
3) 簡單判斷一個(gè)數(shù)是否為0
最后我們?cè)僮鰝€(gè)簡單的模擬,用到我們前面模擬的數(shù)字以及布爾兩種類型來定義一個(gè)方法,判斷傳入的參數(shù)是否為0(是否我們定義的ZERO), 并返回一個(gè)布爾類型(TRUE或者FALSE)的模擬結(jié)果。算法很簡單
def zero?(n) if n == 0 true else false end end
那如何用Lambda表示?我們前面都講過了,ZERO這個(gè)函數(shù)會(huì)接收兩個(gè)參數(shù): 第一個(gè)參數(shù)是函數(shù),第二個(gè)為基礎(chǔ)值,如果傳入的是ZERO函數(shù)的話,我們調(diào)用ZERO的時(shí)候,不管傳入第一個(gè)參數(shù)是什么,調(diào)用結(jié)果都會(huì)直接返回第二個(gè)參數(shù)(也就是基礎(chǔ)值)。
那我們回過頭來想如果把TRUE作為它第二個(gè)參數(shù),把一個(gè)返回FALSE的函數(shù)作為第一個(gè)參數(shù),那當(dāng)我們新函數(shù)接收的是ZERO函數(shù)并且調(diào)用它的時(shí)候不就會(huì)直接返回TRUE了嗎?而其他的方法,如ONE, TWO就會(huì)執(zhí)行-> (x) { FALSE }這個(gè)過程。
可以把代碼寫成
require "./number" require "./boolean" IS_ZERO = -> (proc) { proc[-> (x) {FALSE}][TRUE] } if __FILE__ == $0 p to_boolean(IS_ZERO[ZERO]) p to_boolean(IS_ZERO[ONE]) p to_boolean(IS_ZERO[TWO]) end
運(yùn)行結(jié)果是
1 0 0
只有第一個(gè)ZERO是我們期望的值,最后返回了1(就是true)。其他的都不是我們需要的代表數(shù)值0的Lambda表達(dá)式。
3. 尾聲
這篇文章有點(diǎn)長,主要介紹了Ruby里面的Proc類,以及對(duì)函數(shù)柯里化和Lambda表達(dá)式做了最基本的講解。最后舉了一些例子,用Lambda表達(dá)式來模擬數(shù)字和布爾類型,另外使用我們模擬出來的類型作為基礎(chǔ)來定義一個(gè)實(shí)用的方法IS_ZERO。
本文沒有涉及太多高深的東西,因?yàn)橛泻芏喔呱畹臇|西我還吸收不了,當(dāng)吸收了之后會(huì)繼續(xù)發(fā)文講述。很感謝您的閱讀。以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對(duì)本站的支持。
版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。