當談論引用透明時我們在談論什么

談論到引用透明(Referential Transparency),我們都會聊函數式編程(FP),會聊Effect和Side Effect,會聊純函數(Pure Function)等,這些概念相互關聯,有時甚至彼此引用定義,能夠真正理解他們的含義非常重要。

基本概念

Referential Transparency

引用Wikipedia的定義: An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program's behavior. 即表達式和值可以互相替換,而對程序不產生任何影響。

Side Effect

引用Wikipedia的定義: An operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, which is to say if it has any observable effect other than its primary effect of returning a value to the invoker of the operation.

常見的Side Effect例子:

  • 修改變量
  • 拋出異常
  • 打印日志
  • 讀取寫入文件
Pure Function

Wikipedia的定義較長,這里總結一下,滿足以下兩個條件即為純函數:

  1. 對所有的輸入,相同的輸入都有相同的輸出;
  2. 該Function沒有Side Effect;

這三個概念都是在描述不同Scope的東西,但當我們同在“函數”這一Scope內認為三個概念是等同的,即:

  • 純函數
  • 沒有Side Effect的函數
  • 對任何入參表達式都引用透明的函數
    這三個概念是等同的。由此可得,理解并能夠正確判斷引用透明非常重要。

用幾個例子來理解引用透明

本文都以Scala進行舉例。

1. 判斷 method 是否引用透明
def method(): Int = 1

// One
val value = method()
someFunc(value)

// Two
someFunc(method())

是的。這是一個最基本最簡單的例子,還記得上面對引用透明的定義嗎,其中有三個比較重要的概念:

  1. expression:表達式,即這里的 method()
  2. value: 值,即這里的 value
  3. program:即這里的 someFunc(method())

表達式method()和值value可以相互替換,且對程序someFunc(method())不產生任何影響,因此這里是引用透明的。在對后續較為復雜的場景進行判斷時,我們也可以用這種方式首先清晰的分辨expression,value和program,然后進一步分析。

2. 判斷 method 是否引用透明
def method(): Int = {
  println("evil logging >_<")
  1
}

// One
val value = method()
someFunc(value) + someFunc(value)

// Two
someFunc(method()) + someFunc(method())

不透明。這里expression為method(),value為value,program為 someFunc(method())+someFunc(method())

兩個program雖然返回值都是1,但program1打印了一次日志,program2打印了兩次日志。即表達式和值如果相互替換,會對程序產生行為影響,故引用不透明。

3. 判斷 method 是否引用透明
def method(): Int = {
  println("evil logging >_<")
  1
}

// One
val value = method()
someFunc(value)

// Two
someFunc(method())

引用透明嗎?這里expression為method(),value為value,program為 someFunc(method())

根據定義表達式method()和值value可以互相替換,而對程序someFunc(method())不產生任何影響,那這里就是引用透明了。是嗎?對嗎?例子3和例子2使用了相同的表達式和值,為什么在例子2中不是引用透明的,但例子3中就是引用透明的了呢?

這是一個比較容易混淆的地方,實際上,引用透明只跟expression自己是如何實現的有關,而program只是一個抽象概念,不是某一個具體的例子。如果認為某一個表達式expression是引用透明的,那它應當在任何情況下都是透明的,如果能找到任何一個反例證明其不是引用透明的,那就是引用不透明。正如這里的例子3,我們不能只用例子中給出的program即someFunc(method())來判斷,還需要思考其他program中是否也是如此,使用例子2中的program來判斷就無法滿足條件,因此結論是引用不透明。

用幾個例子來測試是否理解引用透明

根據上面的學習結果來判斷一下下面兩個測試是否引用透明?答案在后面。

測試1: 判斷 method 是否引用透明

def method(input: Int): Int = input

// One
val value = method(1)
someFunc(value)

// Two
someFunc(method(1))

測試2: 判斷 method 是否引用透明

def method(input: Int): Int = input

// One
val value = method({ println("more evil"); 1 })
someFunc(value)
someFunc(value)

// Two
someFunc(method({ println("more evil"); 1 }))
someFunc(method({ println("more evil"); 1 }))

--------------------------------------------------答案分割線-------------------------------------------------

測試1: 引用透明。比較簡單直接,不用解釋。

測試2: 引用透明。但看起來可能有點奇怪,如果這里套用上面的判斷方式expression是method({println(“more evil”); 1}),value是value,program是someFunc(method({println(“more evil”);1})),那么看起來是不透明的,因為執行結果不同,program1只打印一次log,program2打印了兩次log。這里要注意,Scala中代碼塊是可以作為參數的,這里執行結果不同,是因為另一個expression不透明,這里有一個“匿名”表達式{ println("more evil"); 1 },任何一個expression的不透明都會導致program執行結果發生變化。

因此在函數式編程中,使expression純很難,函數時的最終目的是compose所有的表達式,在入口處執行唯一最終組裝出來的內容,要讓大expression是純的,就需要保證每一個子expression都是純的,因此要將其有Side Effect的地方變純,如何變純有很多方式,是另一個話題,最簡單粗暴的方式是包在一個大Monad中,讓所有的Side Effect都被Monad Track住。

如何更好的設計引用透明的表達式

針對測試2的代碼,method本身是引用透明的,但由于Scala代碼能夠將代碼塊作為參數,反而無意中引入了一個新的表達式,從而導致整個代碼不純,如何改進呢?

在FP的開發過程中,在做函數定義時首先要設計時使自己是引用透明的,同時注意不能相信其他部分例如入參是引用透明的,所以需要某種方式限制入參是引用透明的。

=> 改進first round:將入參變lazy,同時保證自己是引用透明的

def method(input: () => Int): () => Int = input

// One
val value = method(() => { println("more evil"); 1 })
someFunc(value)
someFunc(value)

// Two
someFunc(method(() => { println("more evil"); 1 }))
someFunc(method(() => { println("more evil"); 1 }))

這里通過限制入參必須是lazy的方式,限制method引用透明,但注意到,Lazy的入參只能保證正常流程,如果expression執行過程中發生異常呢?

=> 改進second round:引入Either類型

def method(input: () => Either[Error, Int]): () => Either[Error, Int] = input

// One
val value = method(() => {println("more evil"); Right(1)})
someFunc(value)
someFunc(value)

// Two
someFunc(() => {println("more evil"); Right(1)})
someFunc(() => {println("more evil"); Right(1)})

用Either track,保證異常流程返回Left類型,并保證每一個expression的引用透明,這也是為什么我們常見的Scala repo中會大量使用各種Monad的原因之一。

引用透明的好處

這里使用Scala來舉例,因為彷佛在FP的世界會更多的提及引用透明,但實際在代碼設計的過程中,不論OO還是FP,引用透明的設計都能幫助我們得到更多的好處。

Benefit 1: 更易測試

如果被測試的expression是引用透明的,那么輸出只依賴于輸入,編寫測試case時也更加簡單直接,我們只需要傳入已知的入參并進行斷言即可,而Mock Side Effect其實是寫測試過程中很難得一個部分。比如寫一段測試代碼來斷言在console中輸出一段文字。

Benefit 2: 更易重構

如果能夠判斷某個expression是引用透明的,那么我們能夠快速決策該表達式能夠被其value替代,反之亦然。這也是很多大型遺留系統中常見的通病,系統一開始開發時,當然你好我好大家好,怎么寫都無所謂,但隨著時間的推移,代碼量的迭代,當有相關上下文的人員逐漸離開項目,很多代碼都有極大的風險變得難以維護。而如果代碼庫中的expression都是引用透明的,那后續開發人員也可以輕易的進行重構改動。

Benefit 3: 更易理解

如果能夠判斷某個函數是引用透明的,那么我們能夠通過該表達式的輸入輸出以及少量的幾行實現快速理解該expression的設計目的和想要做的事情,一共程度上與“單一設計原則”相呼應。

如果函數不是引用透明的,那么開發人員需要非常注意程序的執行順序,同時需要復雜的各種debugger,inspection等復雜工具來檢查代碼,因為表達式是不透明的,所以整個代碼庫的任何地方任何狀態都有可能發生bug。

Benefit 4: 更好設計

想象一下,如果代碼中的expression都是引用透明的,那么一旦成型后續我們不需要反復多次太多的關注該expression內部的邏輯,我們可以有更多的時間來關于更重要的事情,比如系統架構、代碼質量等。理論上,我們可以直接通過函數簽名的命名、返回類型等快速了解其做了什么功能。因此更多的引用透明能夠時開發者更加高效,并更樂于提高軟件質量。**

?著作權歸作者所有,轉載或內容合作請聯系作者
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容