溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么用C++進(jìn)行函數(shù)式編程

發(fā)布時(shí)間:2021-09-13 17:32:24 來源:億速云 閱讀:201 作者:chen 欄目:編程語言

本篇內(nèi)容主要講解“怎么用C++進(jìn)行函數(shù)式編程”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“怎么用C++進(jìn)行函數(shù)式編程”吧!

用C++進(jìn)行函數(shù)式編程

純函數(shù)

純函數(shù)是這樣一種函數(shù):它只會(huì)查看傳進(jìn)來的參數(shù),它的全部行為就是返回基于參數(shù)計(jì)算出的一個(gè)或多個(gè)值。它沒有邏輯副作用。這當(dāng)然只是一種抽象;在CPU層面,每個(gè)函數(shù)都是有副作用的,多數(shù)函數(shù)在堆的層面上就有副作用,但這一抽象仍然有價(jià)值。

純函數(shù)不查看也不更新全局狀態(tài),不維護(hù)內(nèi)部狀態(tài),不執(zhí)行任何I/O操作,也不更改任何輸入?yún)?shù)。最好不要傳遞任何無關(guān)的數(shù)據(jù)給它——如果傳一個(gè)allMyGlobals指針進(jìn)來,這一目標(biāo)就基本破滅了。

純函數(shù)有許多良好的屬性。

  • 線程安全 使用值參數(shù)的純函數(shù)是徹底線程安全的。使用引用或指針參數(shù)的話,就算是const的,你也應(yīng)當(dāng)知曉一個(gè)執(zhí)行非純操作的線程可能更改或釋放其數(shù)據(jù)的風(fēng)險(xiǎn)。但即便是這種情況,純函數(shù)仍不失為編寫安全多線程代碼的利器。你可以輕松地將一個(gè)純函數(shù)替換為并行實(shí)現(xiàn),或者運(yùn)行多種實(shí)現(xiàn)并比較結(jié)果。這讓代碼的試驗(yàn)和演化都更加便利。

  • 可復(fù)用性 移植一個(gè)純函數(shù)到新的環(huán)境要容易很多。類型定義和所有被調(diào)用的其他純函數(shù)仍然需要處理,但不會(huì)有滾雪球效應(yīng)。有多少次,你明明知道另一個(gè)系統(tǒng)有代碼可以實(shí)現(xiàn)你的需要,但要把它從所有對(duì)系統(tǒng)環(huán)境的假設(shè)中解脫出來,還不如重寫一遍來得容易?

  • 可測(cè)試性 純函數(shù)具有引用透明性(referential
    transparency),也就是說,不論何時(shí)調(diào)用它,對(duì)于同一組參數(shù)它永遠(yuǎn)給出同樣的結(jié)果,這使它跟那些與其他系統(tǒng)相互交織的東西比起來更易于使用。在編寫測(cè)試代碼的問題上,我從來沒有特別盡責(zé);太多代碼與大量系統(tǒng)交互,以至于使用它們需要相當(dāng)精細(xì)的控制,而我常常能夠說服自己(也許不正確)這樣的付出并不值得。純函數(shù)很容易測(cè)試,其測(cè)試代碼就像直接從教料書上摘抄下來的一樣:構(gòu)造一些輸入并查看結(jié)果。每次遇到一小段目前看起來有些奇技淫巧的代碼,我都會(huì)把它拆成一個(gè)單獨(dú)的純函數(shù)并編寫測(cè)試??膳碌氖?,我常常發(fā)現(xiàn)這樣的代碼中存在問題,意味著我撒下的測(cè)試安全網(wǎng)還不夠大。

  • 可理解性與可維護(hù)性 輸入和輸出的限制使得純函數(shù)在需要時(shí)更易于重新學(xué)習(xí),由于文檔不足而隱藏了外部信息的情況也會(huì)更少。

形式系統(tǒng)和軟件的自動(dòng)推理將來會(huì)越來越重要。靜態(tài)代碼分析今天已經(jīng)很重要了,將代碼轉(zhuǎn)換成更加函數(shù)式的風(fēng)格有助于工具對(duì)它的分析,或者至少能讓速度更快的局部工具所覆蓋的問題跟速度慢且更加昂貴的全局工具一樣多。我們這個(gè)行業(yè)講的是“把事情做出來”,我還看不到關(guān)于整個(gè)程序“正確性”的形式證明能成為切實(shí)的目標(biāo),但能夠證明代碼的特定部分不存在特定種類的問題也是很有價(jià)值的。我們可以在開發(fā)過程中多運(yùn)用一些科學(xué)和數(shù)學(xué)成果。

正在修編程導(dǎo)論課的同學(xué)可能一邊撓頭一邊想:“不是所有的程序都要這么寫嗎?”現(xiàn)實(shí)情況卻是“大泥球”(Big Balls of Mud)程序多,架構(gòu)清晰的程序少。傳統(tǒng)的命令式編程語言為你提供了安全艙口,結(jié)果它們就總是被使用。如果你只是寫一些用一下就扔掉的代碼,那就怎么方便怎么來,用到全局狀態(tài)也是常事。如果你在編寫一年之后仍將使用的代碼,那就要將眼前的便利因素跟日后不可避免的麻煩平衡一下了。大部分程序員都不擅長(zhǎng)預(yù)測(cè)日后改動(dòng)代碼將會(huì)導(dǎo)致的各種痛苦。

“純粹性”實(shí)踐

并非所有東西都可以是純的,除非程序只操作自己的代碼,否則到某個(gè)點(diǎn)總要與外部世界交互。嘗試最大限度地推進(jìn)代碼的純粹性可以帶來難以想象的樂趣,然而,要達(dá)到一個(gè)務(wù)實(shí)的臨界點(diǎn),我們需要承認(rèn)副作用到某一刻是必要的,然后有效地管理它們。

即使對(duì)某個(gè)特定的函數(shù)而言,這都不是一個(gè)“要么全有要么全無”的目標(biāo)。隨著一個(gè)函數(shù)的純度不斷提高,其價(jià)值可以連續(xù)增大,而且從“幾乎純粹”到“完全純粹”帶來的價(jià)值要低于從“意大利面條狀態(tài)”到“基本純粹”帶來的價(jià)值。只要讓函數(shù)朝著純粹的目標(biāo)前進(jìn),即使不能達(dá)到完全的純度,也能改善你的代碼。增減全局計(jì)數(shù)器或檢查一個(gè)全局調(diào)試標(biāo)志的函數(shù)是不純的,但如果那是它唯一的不足,它仍然可以收獲函數(shù)式的大部分好處。

避免在更大的上下文中造成最壞的結(jié)果通常比在有限的情形中達(dá)到完美狀態(tài)更加重要??紤]一下你曾經(jīng)對(duì)付過的最令人不爽的函數(shù)或系統(tǒng),那種只有全副武裝才能應(yīng)付的,幾乎可以確定,其中必有復(fù)雜的狀態(tài)網(wǎng)絡(luò)和代碼行為所依賴的各種假設(shè),而這些復(fù)雜性還不只發(fā)生在參數(shù)上。在這些方面強(qiáng)化一下約束,或至少努力防止更多的代碼陷入類似的混亂局面,帶來的影響將比擠壓幾個(gè)底層的數(shù)學(xué)函數(shù)大得多。

朝著純粹性的目標(biāo)重構(gòu)代碼,這一過程通常包含將計(jì)算從它所運(yùn)行的環(huán)境中解脫出來,這幾乎必然意味著更多的參數(shù)傳遞。似乎有點(diǎn)奇特——編程語言中的煩瑣累贅已被人罵夠了,而函數(shù)式編程卻常常與代碼體積的減少相關(guān)。函數(shù)式編程語言寫的程序會(huì)比命令式語言的實(shí)現(xiàn)更加簡(jiǎn)潔,其中的因素與使用純函數(shù)在很大程度上是正交的,這些因素包括垃圾回收、強(qiáng)大的內(nèi)建類型、模式匹配、列表推導(dǎo)、函數(shù)合成以及各種語法糖等。程序體積的減少多半與函數(shù)式無關(guān),某些命令式語言也能帶來同樣的效果。

如果你必須給一個(gè)函數(shù)傳遞十多個(gè)參數(shù),惱火是應(yīng)該的,你可以通過一些降低參數(shù)復(fù)雜性的方法來重構(gòu)代碼。C++中沒有任何維護(hù)函數(shù)純粹性的語言支持,這確實(shí)不太理想。如果有人通過一些不好的方法把一個(gè)大量使用的基礎(chǔ)函數(shù)變得不再純粹,所有使用這一函數(shù)的代碼便統(tǒng)統(tǒng)失去了純粹性。從形式系統(tǒng)的角度聽起來這是災(zāi)難性的,但還是那句話,這并不是一念之惡便與佛無緣的那種“要么全有要么全無”的主張。很遺憾,大規(guī)模軟件開發(fā)中的問題只能是統(tǒng)計(jì)意義上的。

看來未來的C/C++語言標(biāo)準(zhǔn)很有必要增加一個(gè)“pure”關(guān)鍵字。C++中已經(jīng)有了一個(gè)近似的關(guān)鍵字const—一個(gè)支持編譯時(shí)檢查程序員意圖的可選修飾符,加上它對(duì)代碼百利而無一害。D語言倒是提供了一個(gè)“pure”關(guān)鍵字:http://www.d-programming-language.org/function.html。注意它們對(duì)弱純粹性和強(qiáng)純粹性的區(qū)分—要達(dá)到強(qiáng)純粹,輸入?yún)?shù)中的引用或指針需要使用const修飾。

從某些方面來看,語言關(guān)鍵字過于嚴(yán)格了—一個(gè)函數(shù)即使調(diào)用了非純粹的函數(shù)也仍然可以是純粹的,只要副作用不逃出函數(shù)之外即可。如果一個(gè)程序只處理命令行參數(shù)而不操作隨機(jī)的文件系統(tǒng)狀態(tài),那么整個(gè)程序都可看做純粹的函數(shù)式單元。

面向?qū)ο蟪绦蛟O(shè)計(jì)

Michael Feathers(twitter @mfeathers)說:OO通過把移動(dòng)的部件封裝起來使代碼可理解。FP通過把移動(dòng)的部件減到最少使代碼可理解。

“移動(dòng)的部件”就是更改中的狀態(tài)。通知一個(gè)對(duì)象改變自己,這是面向?qū)ο缶幊袒A(chǔ)教材的第一課,在大多數(shù)程序員的觀念中根深蒂固,但它卻是一種反函數(shù)式的行為。將函數(shù)和它們操作的數(shù)據(jù)結(jié)構(gòu)組織在一起,這一基本的OOP思想顯然有其價(jià)值,但如果想在自己的部分代碼中獲得函數(shù)式編程的好處,那么在這些部分,你必須疏遠(yuǎn)一下某些面向?qū)ο蟮男袨椤?/p>

無法聲明為const的類方法從定義上就是不純的,因?yàn)樗鼈円薷膶?duì)象的部分或全部狀態(tài)集合,這一集合可能十分龐大。它們也不是線程安全的,這里戳一下,那里捅一下,一點(diǎn)一點(diǎn)地把對(duì)象置成了非預(yù)期的狀態(tài),這種力量才真正是Bug的不竭之源。如果不考慮那個(gè)隱含的const this指針,從技術(shù)角度const對(duì)象方法仍可看做純函數(shù),但許多對(duì)象十分龐大,大到它本身就足以構(gòu)成一種全局狀態(tài),從而弱化了純函數(shù)的在簡(jiǎn)潔清晰上的一些好處。構(gòu)造函數(shù)也可以是純函數(shù),通常應(yīng)該努力使之成為純函數(shù)——它們接受參數(shù)并返回一個(gè)對(duì)象。

從靈活編程的層面來看,你常??梢杂酶雍瘮?shù)式的方法使用對(duì)象,但可能需要一點(diǎn)接口上的改變。在id Software,我們?cè)惺陼r(shí)間在使用一個(gè)idVec3類,它只有一個(gè)改變自己的void Normalize()方法,卻沒有相應(yīng)的idVec3 Normalized() const方法。許多字符串方法也是以類似的方式定義的,它們操作自身,而不是返回執(zhí)行過相應(yīng)操作的一個(gè)新的副本——比如ToLowerCase()、StripFileExtension()等。

性能影響

在任何情況下,直接修改內(nèi)存塊幾乎都是無法逾越的最優(yōu)方案,而不這么做就難免犧牲性能。多數(shù)時(shí)候這只有理論上的好處,我們一向都在用性能換生產(chǎn)率。

使用純函數(shù)編程會(huì)導(dǎo)致更多的數(shù)據(jù)復(fù)制,出于性能方面的考慮,某些情況下這顯然會(huì)成為不正確的實(shí)現(xiàn)策略。舉個(gè)極端的例子,你可以寫一個(gè)純函數(shù)的DrawTriangle(),接受一個(gè)幀緩存(framebuffer)參數(shù)并返回一個(gè)全新的畫上三角形的幀緩存作為結(jié)果??蓜e這么做。

按值返回一切結(jié)果是自然的函數(shù)式編程風(fēng)格,然而總是依靠編譯器實(shí)施返回值優(yōu)化會(huì)對(duì)性能造成危害,因此對(duì)于函數(shù)輸出的復(fù)雜數(shù)據(jù)結(jié)構(gòu),傳遞引用參數(shù)常常是合理的,但這么也有不好的一面:它阻止你將返回值聲明為const以避免多次賦值。

很多時(shí)候人們都有強(qiáng)烈的欲望去更新傳入的復(fù)雜結(jié)構(gòu)中的某個(gè)值,而不是復(fù)制一份副本并返回修改后的版本,但這樣等于舍棄了線程安全保障,因此不要輕易這么做。列表的產(chǎn)生倒是一種可以考慮就地更新的合理情形。往列表中追加新的元素,純函數(shù)式的做法是返回尾端包含新元素的一個(gè)全新列表副本,原先的列表則保持不變。真正的函數(shù)式語言都在實(shí)現(xiàn)上運(yùn)用了特別手法,從而使這種行為的后果沒有聽上去那么糟糕,但如果在典型的C++容器上這么做,那你就死定了。

一項(xiàng)重要的緩解因素是,如今性能意味著并行程序設(shè)計(jì),相比單線程環(huán)境,并行程序即使在性能最優(yōu)的情形中也需要更多的復(fù)制與合并操作,因此復(fù)制造成的損失減少了,而復(fù)雜性的降低和正確性的提高這兩方面的好處相應(yīng)增加了。例如,當(dāng)開始考慮并行地運(yùn)行一個(gè)游戲世界中的所有角色時(shí),你就會(huì)漸漸明白,用面向?qū)ο蟮姆椒▉砀聦?duì)象,這在并行環(huán)境中難度很大?;蛟S所有對(duì)象都引用了世界狀態(tài)的一個(gè)只讀版本,而在一幀結(jié)束時(shí)卻復(fù)制了更新后的版本……嗨,等一下……

如何行動(dòng)

在自己的代碼庫(kù)中檢查某些有一定復(fù)雜度的函數(shù),跟蹤它能觸及的每一比特外部狀態(tài)以及所有可能的狀態(tài)更新。即使對(duì)它不做一點(diǎn)改動(dòng),把這些信息放入一個(gè)注釋塊就已經(jīng)是極好的文檔了。如果函數(shù)能夠——比方說,通過渲染系統(tǒng)觸發(fā)一次屏幕刷新,你就可以直接把手舉在空中,聲明這個(gè)函數(shù)所有的正副作用已經(jīng)超出了人類的理解力。你要著手的下一項(xiàng)任務(wù)是基于實(shí)際執(zhí)行的計(jì)算從頭開始重新考慮這個(gè)函數(shù)。收集所有的輸入,把它傳給一個(gè)純函數(shù),然后接收結(jié)果并做相應(yīng)處理。

調(diào)試代碼的時(shí)候,讓自己著重了解那些更新的狀態(tài)和隱藏的參數(shù)悄然登場(chǎng),從而掩蓋實(shí)際動(dòng)作的部分。修改一些工具對(duì)象的代碼,讓函數(shù)返回新的副本而不是修改自身,除了迭代器,試著在自己使用的每個(gè)變量之前都加上const。

到此,相信大家對(duì)“怎么用C++進(jìn)行函數(shù)式編程”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

c++
AI