一個函數的自白

njsq5953 6年前發布 | 36K 次閱讀 軟件開發

我是——編程世界的函數,不是數學中的冪,指,對和三角函數等等,但是和f(x)又有著千絲萬縷的關系。

我是代碼中的最小執行組織,但不是最小執行單元。最小的執行單元是一條條語句,這些語句有機地組合起來完成一個或多個功能并且可以復用,這才是我——函數。

內存與堆棧和我之間是啥關系?
有無參數的我有何異同?
我的簡潔性?復雜度如何評估?
我的高階與遞歸有啥區別?
我的回調和匿名是一回事么?
對象中的方法是我么?
控制對象的行為方式有哪些呢?
為什么說類型錯誤只是異常處理的一種方式?
面對數據密集型應用和并發場景,我有何作用?
......

且聽一個函數的自白,從函數的角度看編程的方式。

我眼中的大環境——內存

空山不見人  但聞人語響

代碼最終都要加載到內存中執行,組成函數的代碼同樣如此。內存容量是受限的,需要考慮一個函數在內存中所需要處理和生成的數據量。內存中沒有變量名或簽名的內存地址,只有以數字表示的內存地址。考慮“什么將存入內存”,“什么存入硬盤”以及“何時將內存內容存入硬盤”,這些考量都與性能優化息息相關。

而現代編程語言強調透明的內存管理,關注處理中不斷增長的數據規模,這很容易失去對內存消耗的控制,從而導致運行時性能的下降。對我所采用的不同內存使用策略,所帶來的不同結果給予適當的關注,是件有意義的好事情。

我的運行環境——堆棧

明月松間照 清泉石上流

由于內存中的東西太多了,于是把我運行環境中的內存稱為堆棧。對于棧,所有操作都針對棧中的數據,堆用于存儲后續操作所需的數據。堆中數據需要入棧進行操作,最終出棧回到堆。棧溢出是內存耗盡的情況。

通常對程序員來說,棧是不可見的,幾乎在每個現代的編程語言中,棧都是支持線程執行的內存區域。 堆是許多現代編程語言實現中的另一重要內存區域,用于動態內存的分配和釋放,例如創建列表和對象。不要將這里的堆棧與數據結構中的概念混淆,數據結構中的堆是一個基于樹的數據結構。

有一種執行環境叫棧機器,使用了棧而不是寄存器來支持程序表達式的計算,許多現代虛擬機都是這樣的,例如JVM。

我源自面向過程的抽象——過程函數

大事化小  小事易了

程序員們通常會把復雜問題分解為若干個較小的容易問題,這是一種面向過程的抽象,一個過程是實現某一功能的我,我可能接收輸入,但未必產生輸出。極端一點的我既沒有輸入參數,也沒有返回值,是純粹的過程函數。每個過程函數獨立做一件事,完成一項任務,過程函數一般不返回數據,只操作共享數據。人們把這種編程方式叫做結構化編程。

作為過程函數的我一般用全局變量來共享狀態,我會改變或增加共享狀態。過程函數可能不是冪等的,而缺乏冪等性被很多人認為是編程錯誤的一個來源。

冪等性: 若一個函數或過程是冪等的,對其進行多次調用將觀察到同樣的效果,
與一次調用的效果是相同的。

而且,采用全局變量也一直被認為是一個餿主意,然而在系統層面,架構中的組件共享其實和全局變量類似,這讓我有時候感到無語。

我和伙伴們組合起來——復合嵌套

但遠山長 云山亂 曉山青

當程序員把復雜問題分解成若干小問題的時候,一般都是把我接收的輸入形成輸出,這樣就可以把所有任務都視為輸入集合到輸出集合的映射關系了。嗯,f(x)的味道來了。

這時候,函數間無狀態共享,類似于數學中的復合函數。特殊的,把函數作為輸入或者輸出的函數被稱為高階函數。多個參數的函數可以通過柯里化技術轉化為一系列的單參數高階函數。

這種串行話處理的一個知名應用就是Unix Shell 中的管道,編譯器及其他語言處理器也很適合這種方式,且傾向于利用圖和樹等數據結構組合過程。類似的,還有單元測試和開發部署。

構成我的代碼簡潔性

天街小雨潤如酥 草色遙看近卻無

程序員通常把簡潔作為編程技藝的標志之一,但是當代碼簡潔成為唯一目標的時候,每行代碼又往往會變得冗長且難以理解,這是因為需要利用編程語言的許多高級特性和庫來實現。無論好壞,源代碼行數(SLOC)還是可以用來估算成本和開發效率的,也可以評估可維護性和其他許多管理指標。例如,CoCoMO就是基于SLOC的成本評估模型,至今還被采用。

基于代碼行數的簡潔性往往取決于已經構建好的第三方庫和方法,當然,運用得當,簡潔性也會帶來優雅且可讀性強的代碼。一般地,可以信任核心庫,但對第三方庫的使用需謹慎。

面向簡潔性,融合高階函數,以至于一切皆為函數, 甚至形成了新的編程模式——函數式編程。純粹的函數式編程語言,以Haskell 為代表。

我的復雜性度量

吾嘗觀竅妙  渠敢譏雜駁

我的復雜程度是由程序員決定的,可以非常簡單,也可以非常復雜。一般地,可以采用一種叫圈復雜度的方式來評估我的復雜程度。圈復雜度是一個用于衡量代碼復雜度的方式,主要是通過描述控制流路徑的數量來表示復雜度。圈復雜度把程序看成一個有向圖,計算公式如下:

CC = E -N +2P  E是邊數  N 是節點數,P是節點出口數。

圈復雜度可以衡量程序的復雜性,同樣適用于函數。

我調用我自己——遞歸函數

知人者智 知己者明

由于很多的問題都可以使用歸納法進行建模,就象學生時代的數學歸納法那樣,即已知n=0的情況和n到n+1的推導規則,對問題進行求解。一般地,在編程世界中,歸納法用遞歸函數表示。遞歸函數就是自己調用自己,一直在棧中操作,如果遞歸層次過深的話,會導致棧溢出問題的出現。

在許多編程語言中,尾遞歸優化解決了遞歸調用中的棧溢出問題。 尾調用是指一個函數里的最后一個動作是一個函數調用,即在函數尾部發生的遞歸調用。尾遞歸即在函數尾部發生的遞歸調用,尾遞歸發生時,程序語言的處理器可以安全地刪除先前的棧記錄,因為該調用返回時棧中不需要繼續其他操作,這就是尾遞歸優化,尾遞歸優化有效地將遞歸函數轉為迭代,節省了時間和內存。

需要注意的是,python中并不對尾遞歸進行優化,一般要對調用深度進行限制。

下一個是我的自動調用——回調和匿名

忽如一夜春風來,千樹萬樹梨花開。

一般地,函數間的調用是顯式的,即一個函數執行完畢在執行下一個函數。 但有這樣一種使用場景,一個函數有一個額外的參數,通常是最后一個,這一參數是另一個函數,在函數執行到末尾的時候,作為參數的函數也會被調用。

參數函數作為輸入,也作為當前函數的輸出,函數間的這種管道式傳遞可以解決處理規模較大的問題,即管道中下一個被調用的函數會當作當前參數的輸出。 這種后續傳遞,通常使用匿名函數。

如果參數函數并不是在末尾被調用,而是在特定的事件或條件發生時由另外的一方調用,參數函數用于對該事件或條件進行響應,通常使用回調函數。在C/C++中,回調函數就是一個通過函數指針調用的函數,把函數的指針(地址)作為參數傳遞給另一個函數,用這個指針來調用其所指向的函數。回調函數一般使用通知機制。典型的場景如編譯器優化,處理程序的正常流程和異常流程,解決單線程語言的IO阻塞問題等等。

需要注意的是,大量的回調函數可能會增加復雜性,使代碼的可讀性變差,例如JavaScript 中的回調地獄。

我們長在對象上就成了——方法

千人同心   則得千人之力

我們都是要處理數據的,可以把數據封裝起來形成一個可以修改的數據抽象。 將若干函數逐個綁定在數據抽象上,建立函數的調用順序,查看數據的最終結果,這是一種面向數據的過程封裝抽象,特點在于綁定操作將數據抽象作為參數,調用指定函數,并將函數返回值賦回。

如果將問題分解成某些問題領域的相關對象,每個對象都是一個數據封裝,處理過程暴露在外,數據只能通過這些過程訪問,不可直接訪問,每個對象可以重新定義在其他對象中已經定義好的過程。這種長在對象上的函數又叫——方法。

對象的本質是過程分享對象內的數據,與類和繼承聯系在一起。繼承實際上定義了對象間的從屬關系,從而有了抽象類,基類,實例,覆蓋,子類,單例等等。

對象中我們的遠程調用

萬里云霄送君去 不妨風雨破吾廬

隨著網絡應用的發展,網絡中某個節點的軟件希望引用其他遠程節點的對象實例,并且把遠程對象的方法當作本地方法來使用。于是,誕生了分布式對象系統的平臺和框架,例如CORBA 和RMI。這些分布式對象系統有一個前提假設,就是需要為所有的分布式組件采取通用的編程語言或基礎架構,但通用基礎架構的假設是難以成立的。CORBA的統一化方式退出了舞臺,它和web技術的大規模應用相沖突,因為后者基于的是不同技術的大規模系統設計方法。

但是對象方法的遠程調用還是有使用場景的,如果每個對象是僅公開一個過程的數據封裝,即能夠接收和發送消息。消息分發機制能將消息發送至另一個封裝。 對象向外界公開一個函數————接收消息的函數而并非一系列函數,其他的數據和函數被隱藏在內部, 接口函數處理能夠被對象解釋的消息;一些無法被對象解釋的消息,則被忽略或生成某種形式的錯誤;另一些消息可能并不由該對象直接處理,而是由其他與接收對象相關的對象處理。

對于消息分發機制,帶來的是某個對象使用其他對象的方法執行過程的能力。消息分發是接收消息、解釋消息并確定執行步驟的過程。該過程可能是方法執行、錯誤返回或者向其他對象轉發消息。

對象的域——我們與對象中數據的關系

悠然一曲泉明調 淺立閑愁輕閉門

對象的域一般是指鍵與簡單值的映射,對象中的一些方法成為了鍵與值之間的函數映射,構造函數是最先被調用的方法。閉域是指每個對象是一個鍵值映射,其中某些值是我們這些函數。對象的方法引用對象自身的鍵,使得映射是封閉域的。

閉域解釋了對象編程中的一個特色——原型。原型常見于無類面向對象語言中的對象。原型帶有自己的數據和函數,可以自由地改變而不影響其他對象,新原型可以通過復制已存在的原型獲得。

閉域風格的缺點在于沒有訪問控制,只能由程序員來約束,通過鍵來檢索字典等同于向字典發消息。

對象的抽象——抽象對象

草枯鷹眼疾 雪盡馬蹄輕

抽象對象是將大問題分解為問題域相關的對象抽象。抽象對象定義了對象的抽象行為,具體對象以某種方式與抽象對象綁定,綁定機制可以不定,應用程序的其他部分并不依賴對象的內容,而依賴對象的行為。

在設計模式中,適配器模式的目的與抽象對象是一致的,隔離了應用程序與具體的功能實現。抽象對象在大型系統設計中舉足輕重,抽象對象的實現根據涉及的具體編程語言而定。Java中的抽象對象是接口,可以在類型上參數化;Haskell是一種強類型的純函數語言,抽象對象表現為類型類;C++擁有抽象類,連同模版一起完備地提供了參數化抽象對象的概念。

控制對象中我們的另一方式——控制反轉

春潮帶雨晚來急  野渡無人舟自橫

還有另一種對象行為的控制方式,利用對象和模塊等不同形式的抽象,將大問題分解成若干個實體,這些實體不能被直接調用,而是為其他實體提供接口,使其他實體能夠注冊回調函數,這些實體中的函數調用是通過其他實體注冊過的回調函數來完成的。

這種行為控制方式不會在程序中顯式地調用函數,而是通過反轉關系,使調用者可以同時觸發多個行為,是一種能夠在框架中觸發任意應用代碼的機制,這就是控制反轉。

控制反轉是分布式系統設計的一個重要概念,源于異步硬件中斷,回調函數可以同步執行也可以異步執行。在事件發生時,不同網絡節點間的回調函數不用長輪詢,從而,事件驅動框架應運而生。

類似的,可以使用一個用于發布和訂閱事件的基礎結構,對象實體中的我們負責訂閱和發布事件,基礎結構負責事件的管理和分發。 這樣的基礎結構常與異步組件共同使用,也可能包含更復雜的事件結構,支持更精確的事件過濾,相當于控制反轉的輕量級形式。

審視自身,進而改變我自己

見賢思齊焉 見不賢而內自省也

在程序設計的過程中可以將程序自身也一起考慮。有一種抽象的方式是可以獲取自身以及其他的抽象信息,但不能改變這些信息。程序獲取自身信息的能力叫做自省,支持自省的語言有java,python,javascript,以及PHP等,而C/C++不支持自省。其中,python 有強大的自省能力,如callable,inspect等。 然而,使用自省使得程序變得不直觀,甚至難以理解,有利有弊吧。

反射使用了自省,程序在運行時可以通過增加抽象、變量等方式進行自我修改。反射的目的是要求程序能自我修改,ruby支持完備反射,python 和javascript在限制條件下支持反射,而java只支持小部分的反射。在設計過程中,當無法預期代碼被修改方式的時候,會使用反射。

如果將問題的切面增加到主程序中,但不改變這種抽象方式的源碼和使用該抽象的代碼段,再通過一個外部綁定機制將這種抽象形式和切面綁定在一起,這就是AOP。AOP是一種受限制的反射,目的是在已有程序的指定代碼前后插入任意代碼。 切面主要關注可能被分散到應用程序中的代碼,這些代碼通常會影響許多組件,典型的切面如 profiling 和 tracing。

把我們有組織的固定下來充分復用——插件

但要前塵減   無妨外相同

如果把我們有組織的固定下來,所有或部分被預編譯后通常會自成一體,主程序和每個包單獨編譯,主程序在開始時動態地加載這些包,使用動態加載包中的函數和對象,無需知道具體事項,這就是插件。

插件又叫plugin或者addon,不需重新編譯,就可以將一系列功能加入到正在執行的應用程序中。通過一個外部定義來說明哪些包需要被加載,通常是配置文件,路徑約定,用戶輸入或其他運行時加載外部代碼的機制。現代操作系統的動態鏈接庫dll/so 就是插件風格,需要注意的是存在配置深淵。

對于分布式體系結構和支持第三方擴展的獨立應用程序,帶有反射能力的編程語言使得在運行時鏈接組件變得可行并且非常簡單。Java Spring框架就支持由反射機制帶來的插件化開發,稱為“依賴注入”和“插件”,插件一般使用描述性配置語言如INI和XML。

據說,插件是軟件進化和定制的核心。

我錯了?!——異常處理

此情可待成追憶  只是當時已惘然

異常是在程序運行中超出正常預期的情況。我和每一個伙伴都會檢查自身參數的合理性,當參數不合理時,返回合理的結果或者給參數指定合理的值。所有的代碼塊都會檢測可能存在的錯誤,當錯誤發生時,跳過代碼塊,設置合理的狀態并繼續執行函數的其他部分。

通常防御式編程能為用戶帶來較好的體驗,每個過程和函數都檢測自身參數的合理性,若參數不合理,程序停止運行。另外,當錯誤發生時,最好將上下文相關的信息寫入日志,同時將錯誤傳遞回函數調用鏈。如果對異常采取消極態度,至少也應該通知各方正確的使用方式,以及停止運行的原因。

全局捕獲是我們另一種處理異常的方法,在調用其他函數時,程序僅檢測能夠提供有意義反饋的錯誤。這種異常處理在函數調用鏈中位于較上層,僅在程序最外層進行異常處理,無視異常時間發生的位置。

無論在哪里捕獲異常,調用棧都是異常信息的一部分,除非局部存在有意義的處理方式,更好的做法是將異常返回到函數調用鏈的上游。

我眼中的類型錯誤

堪嗟歲月蹉跎久   卻悔塵寰錯誤多

對于輸入參數而言,一般地,我會聲明所期待的參數類型。如果調用方沒有傳送預期類型的參數,則會產生類型錯誤,這時將不再執行。類型不匹配是指我得到的值類型與所期待的值類型不符;或者一個伙伴返回了一個特定類型的值,但該值稍后被調用者當作其他類型的值使用。不同類型的值通常被分配不同大小的內存空間,這意味著當發生類型不匹配時,內存可能被重寫而變得不一致,這就是這類異常的問題所在。

所有現代高級編程語言都有一個類型系統,在開發和執行過程中的不同節點檢測數據類型。靜態類型的語言如Java 和 Haskell,動態類型如JS,python等等。參數類型分為顯示類型和隱式類型,相關的邏輯操作包括強制類型裝換,類型推理和類型安全。

我可以專注于數據計算

無邊落木蕭蕭下  不盡長江滾滾來

如果我專注于數據計算的話,會有一些特殊的約束。首先是隔離,核心函數不要有任何副作用,所有IO行為都最好和純粹的函數明確區分開來,所有包含IO的函數最好從主程序中調用。這樣做的主要目的是避免或最小化IO操作,盡量隔離IO操作,因為IO操作在大型系統中是個大問題。

典型的數據密集型應用是數據庫,數據獨立于程序執行,能夠被多個程序使用,存儲易于快速檢索,通過對數據查詢來解決問題。對某些數據的規范處理可能包含數據列及公式,某些數據可能由其他數據通過公式決定。當數據改變時,相關聯數據將自動改變,例如數據庫中的視圖,觸發器和存儲過程等。

如果數據的可用形式是流,我就是數據流的過濾器/變換器,根據下游的需求,對上游的數據進行處理。流式處理適用于支持生成器的語言,對于Java 這些不支持生成器的語言,可以通過迭代器實現。

我也可以適應并發處理場景

水深草茂群蛙怒   日出風和宿麥秋

面向高并發的場景,每個對象都存在一個隊列,用于放置向其他對象發送的消息。每個對象都是一個數據封裝,僅公開其接收消息的接口,用于前述隊列,每個對象運行于獨立的線程中。每個對象主動輪詢各自的消息隊列,一次處理一條消息,當隊列為空時,阻塞當前線程。這大概就是Actor模型了。

當一個或多個并發單元,同時配備了一個或多個數據空間,數據空間用于并發單元的數據存儲和檢索,并發單元之間只能通過數據空間進行數據交換。 尤其是當任務需要橫向開展的時時候,這種基于數據空間交換的方式,也是適合于數據密集型并行處理的。

對于流式數據而言,輸入的數據流被分成若干塊,map函數對每個數據塊運用worker,通常過程是并行的。而 reduce 函數得到各個worker函數的結果,并重組成相關輸出。 這種基于MapReduce的函數處理方式,極其適用于可以單獨拆分和處理數據的數據密集型操作,其局部結果在最后被重組。如果將worker 函數的結果重組,第二次map以重組后的數據作為reduce函數的參數的話,就是雙重MapReduce方式了。

一般的,map可以并行,而reduce不行,hadoop將map的結果列表重組并打亂,使后續reduce函數的調用可以對數據重組進行修改,從而reduce也成了可以并行化的。

至于restful,不過是將我們函數的思想用于子系統,更關注擴展性,去中心化和獨立組件開發而已。

結尾的告白

誰人在處望風煙  蕓蕓眾生吾自潛

函數是程序中最小的有序時空,運轉于內存堆棧。

關于參數,沒有輸入參數和返回值就是純過程函數, 而同時有參數和返回值才可能實現冪等性。多參數的我可以柯里化為單參數高階函數,而參數中是函數的話可以形成處理管道,或者回調函數。我自己調用自己,就是遞歸函數。

關于對象,我長在對象上變成了方法,進一步可以提升為抽象對象。對象間的遠程調用一般用消息機制,對象間的行為操控可以說是控制反轉,而通過對本身的自省可以形成反射,AOP 可以看作有條件的反射。對于插件,幾乎是函數組裝之集大成者。

關于錯誤,一般采用防御式編程,也可以采用消極的方式,無論是否采用全局捕獲,調用棧都是異常信息的重要部分。

關于特定場景,不論是密集數據計算還是高并發情況,都最終落實到函數的層面。

 

來自:http://mp.weixin.qq.com/s/1VtITPEufTMcI42WFEhoEw

 

 本文由用戶 njsq5953 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!