當(dāng)談?wù)撘猛该鲿r(shí)我們?cè)谡務(wù)撌裁?/h2>

談?wù)摰揭猛该?Referential Transparency),我們都會(huì)聊函數(shù)式編程(FP),會(huì)聊Effect和Side Effect,會(huì)聊純函數(shù)(Pure Function)等,這些概念相互關(guān)聯(lián),有時(shí)甚至彼此引用定義,能夠真正理解他們的含義非常重要。

基本概念

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. 即表達(dá)式和值可以互相替換,而對(duì)程序不產(chǎn)生任何影響。

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.

常見(jiàn)的Side Effect例子:

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

Wikipedia的定義較長(zhǎng),這里總結(jié)一下,滿足以下兩個(gè)條件即為純函數(shù):

  1. 對(duì)所有的輸入,相同的輸入都有相同的輸出;
  2. 該Function沒(méi)有Side Effect;

這三個(gè)概念都是在描述不同Scope的東西,但當(dāng)我們同在“函數(shù)”這一Scope內(nèi)認(rèn)為三個(gè)概念是等同的,即:

  • 純函數(shù)
  • 沒(méi)有Side Effect的函數(shù)
  • 對(duì)任何入?yún)⒈磉_(dá)式都引用透明的函數(shù)
    這三個(gè)概念是等同的。由此可得,理解并能夠正確判斷引用透明非常重要。

用幾個(gè)例子來(lái)理解引用透明

本文都以Scala進(jìn)行舉例。

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

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

// Two
someFunc(method())

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

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

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

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())

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

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())

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

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

用幾個(gè)例子來(lái)測(cè)試是否理解引用透明

根據(jù)上面的學(xué)習(xí)結(jié)果來(lái)判斷一下下面兩個(gè)測(cè)試是否引用透明?答案在后面。

測(cè)試1: 判斷 method 是否引用透明

def method(input: Int): Int = input

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

// Two
someFunc(method(1))

測(cè)試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 }))

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

測(cè)試1: 引用透明。比較簡(jiǎn)單直接,不用解釋。

測(cè)試2: 引用透明。但看起來(lái)可能有點(diǎn)奇怪,如果這里套用上面的判斷方式expression是method({println(“more evil”); 1}),value是value,program是someFunc(method({println(“more evil”);1})),那么看起來(lái)是不透明的,因?yàn)閳?zhí)行結(jié)果不同,program1只打印一次log,program2打印了兩次log。這里要注意,Scala中代碼塊是可以作為參數(shù)的,這里執(zhí)行結(jié)果不同,是因?yàn)榱硪粋€(gè)expression不透明,這里有一個(gè)“匿名”表達(dá)式{ println("more evil"); 1 },任何一個(gè)expression的不透明都會(huì)導(dǎo)致program執(zhí)行結(jié)果發(fā)生變化。

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

如何更好的設(shè)計(jì)引用透明的表達(dá)式

針對(duì)測(cè)試2的代碼,method本身是引用透明的,但由于Scala代碼能夠?qū)⒋a塊作為參數(shù),反而無(wú)意中引入了一個(gè)新的表達(dá)式,從而導(dǎo)致整個(gè)代碼不純,如何改進(jìn)呢?

在FP的開(kāi)發(fā)過(guò)程中,在做函數(shù)定義時(shí)首先要設(shè)計(jì)時(shí)使自己是引用透明的,同時(shí)注意不能相信其他部分例如入?yún)⑹且猛该鞯模孕枰撤N方式限制入?yún)⑹且猛该鞯摹?/p>

=> 改進(jìn)first round:將入?yún)⒆僱azy,同時(shí)保證自己是引用透明的

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 }))

這里通過(guò)限制入?yún)⒈仨毷莑azy的方式,限制method引用透明,但注意到,Lazy的入?yún)⒅荒鼙WC正常流程,如果expression執(zhí)行過(guò)程中發(fā)生異常呢?

=> 改進(jìn)second round:引入Either類(lèi)型

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類(lèi)型,并保證每一個(gè)expression的引用透明,這也是為什么我們常見(jiàn)的Scala repo中會(huì)大量使用各種Monad的原因之一。

引用透明的好處

這里使用Scala來(lái)舉例,因?yàn)獒莘鹪贔P的世界會(huì)更多的提及引用透明,但實(shí)際在代碼設(shè)計(jì)的過(guò)程中,不論OO還是FP,引用透明的設(shè)計(jì)都能幫助我們得到更多的好處。

Benefit 1: 更易測(cè)試

如果被測(cè)試的expression是引用透明的,那么輸出只依賴(lài)于輸入,編寫(xiě)測(cè)試case時(shí)也更加簡(jiǎn)單直接,我們只需要傳入已知的入?yún)⒉⑦M(jìn)行斷言即可,而Mock Side Effect其實(shí)是寫(xiě)測(cè)試過(guò)程中很難得一個(gè)部分。比如寫(xiě)一段測(cè)試代碼來(lái)斷言在console中輸出一段文字。

Benefit 2: 更易重構(gòu)

如果能夠判斷某個(gè)expression是引用透明的,那么我們能夠快速?zèng)Q策該表達(dá)式能夠被其value替代,反之亦然。這也是很多大型遺留系統(tǒng)中常見(jiàn)的通病,系統(tǒng)一開(kāi)始開(kāi)發(fā)時(shí),當(dāng)然你好我好大家好,怎么寫(xiě)都無(wú)所謂,但隨著時(shí)間的推移,代碼量的迭代,當(dāng)有相關(guān)上下文的人員逐漸離開(kāi)項(xiàng)目,很多代碼都有極大的風(fēng)險(xiǎn)變得難以維護(hù)。而如果代碼庫(kù)中的expression都是引用透明的,那后續(xù)開(kāi)發(fā)人員也可以輕易的進(jìn)行重構(gòu)改動(dòng)。

Benefit 3: 更易理解

如果能夠判斷某個(gè)函數(shù)是引用透明的,那么我們能夠通過(guò)該表達(dá)式的輸入輸出以及少量的幾行實(shí)現(xiàn)快速理解該expression的設(shè)計(jì)目的和想要做的事情,一共程度上與“單一設(shè)計(jì)原則”相呼應(yīng)。

如果函數(shù)不是引用透明的,那么開(kāi)發(fā)人員需要非常注意程序的執(zhí)行順序,同時(shí)需要復(fù)雜的各種debugger,inspection等復(fù)雜工具來(lái)檢查代碼,因?yàn)楸磉_(dá)式是不透明的,所以整個(gè)代碼庫(kù)的任何地方任何狀態(tài)都有可能發(fā)生bug。

Benefit 4: 更好設(shè)計(jì)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容