函式的正確用法

更新於 發佈於 閱讀時間約 7 分鐘

前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(procedure)。或許是在這種觀念下的影響,很多陳舊的函式庫,例如MFC,都會利用參數傳回結果,比較現代的函式庫則比較少這麼做了。對於函數式編程,函式就像是函數,函式的參數代表輸入,回傳值則是輸出。在擁有代數資料類型的支援下,回傳多個值變得非常容易,不再需要透過參數輸出結果。函數式編程也建議少用變數修改(mutation),因此也不應該只為了回傳結果而使用參數傳入參考。


函式最原始的用途是重用程式碼。重用程式碼的目的是將不同地方之中相同的程式邏輯抽取出來,讓我們不用每次都寫一樣的東西。然而在現代應該依據概念定義函式,而非只為了重用程式碼而定義。有時候程式碼一樣不一定代表它們有相同的概念。反之如果它的概念很重要,就算沒有重用的必要,也應該抽取成函式。然而有些人建議程式碼不應超過三層縮排,如果超過了應該考慮用函式抽取出來,我認為這種建議很蠢。把一段程式碼抽取出來會增加程式碼的「深度」,若要知道這個函式是在做什麼,除非它的名稱非常明瞭,否則就必須閱讀它的說明文件,如果沒有就得找原始碼來看。現代編輯器大部分都帶有提示與跳轉功能,因此這麽做並不會造成太多閱讀上的困難。但一般編輯器也會帶有程式碼折疊的功能,只要在複雜的操作前加上註解,再把它折疊起來就能達到相同或更好的效果。縮排層數一般代表流程控制的層數,太多層的流程控制的確會使理解程式邏輯更困難,然而直接拿縮排層數判斷是不準確的,有時我們只是想要控制變數作用範圍而使用區塊,它雖然會增加一層縮排,但反而會減少流程控制複雜度。透過一些技巧,例如及早返回(early return)或是改變條件判斷的順序,可以減少層數,這麽做的確是好的。然而使用物件導向設計時很多人會傾向使用多型把判斷狀態與操作交給多型實現(polymorphism > if/switch),這種做法多了一層抽象,方法不應根據這種狀況而增加。


把程式碼片段抽取成函式時,需要把它使用到的變數轉為參數傳進去,或是實作成物件的方法,讓它自動獲取成員變數的存取權。第一種方法可能會讓參數列表太長,這不是好事;第二種方法依賴於物件導向的支援,而且不適用於關係到區域變數時。如果這些操作牽涉到變數的修改,就更不應該抽取成函式。函式應該要把副作用封裝在內部,否則會增加隱性狀態。另外把它抽取成函式需要把相關的上下文也包含進去,也就是說明這個函式應該在怎樣的情境下使用,否則你的同事可能會誤會它的用途。當你把它抽取成函式之後,如果不是私有函式,就不應該再改變它的定義與使用情境,而這非常不利於修改。如果你沒有遵守這點,當你突然想要修改這部分的一些操作邏輯而修改了這個函式的一點定義,其他使用這個函式的程式碼很有可能就會因此出錯。這種抽取函式的目的本質上就違反了依賴抽象的原則。最好的方法是直接定義在方法內部,如此一來只有這個方法能存取,因此我們也不需說明使用情境或是太過在意是否依賴抽象,這在Haskell很常看到(where clause)。如果你只是為了減少縮排而抽取函式,這種函式的情境通常都非常特定,或是你在寫的時候認為有泛用性,但沒有意識到你利用了當下情境的一些假設。就算你有理解這點,應該也沒有人會給他寫這麽複雜的說明註解,畢竟你只是為了減少縮排層數。


就算他有一個明確的抽象語義,也不一定有抽取成函式的價值,只有當他足夠簡單或是很重要,才應該抽取成函式。過多過於細緻的函式也會增加開發者的負擔,這會讓使用者需要記一堆非常特例的用法與概念,就只為了讀懂一段程式碼。我們不應該擔心縮排層數的問題,我們更應該擔心程式邏輯與概念的可理解性,我們應該盡量避免「概念污染」,過多的概念會增加整個系統的理解難度。設計程式時應該以讓他人能更容易理解而設計(除非你是為了優化效能而設計),因此適度地把一些相近的概念模糊化成一個統一的概念是好的,就像Lua把陣列和字典融合成table一樣(雖然我不認為這麽做是好的)。如果概念就是這麼複雜,應該考慮將這些概念封裝在內部以隱藏複雜度。就像一些排序演算法會用到排序網路的概念,然而使用排序函式時我們不需要理解它是什麼,因為它被封裝在內部了。


函式在函數式編程中是重用程式碼的唯一方法,而物件導向還可以使用繼承「重用」資料結構。一些函數式程式語言也能做到類似繼承資料結構的效果,但一般都是使用組合。繼承重用的是一套運行架構,而不只是一個執行程序。例如Stream類別是一套能進行讀寫的運行架構,我們可以透過它重用讀寫的整套邏輯,例如讀取指定長度的資料,或是Stream的狀態管理。它定義了一套運作規則,覆寫方法時都應遵守這套規則,其他方法會根據你覆寫的方法實現更複雜的操作。這種重用整個架構的方法可以看作是由繼承者提供一套互相關聯的函式,並透過組合這些基礎函式做出更進階的函式,因此那些可覆寫的方法(輸入)跟不可覆寫的方法(輸出)有結構上的不同。這種做法需要以類別作為輸入與輸出函式的載體。在函數式編程中,則是直接提供進階函式,但必須透過trait/typeclass作為輸入基礎函式的載體。它並不像類別把所有輸出函式都搜集起來到一個地方,因此看起來比較鬆散,但你仍然可以自己放到模組或命名空間裡面。這種風格用物件導向的介面也做得到,差別在於它更具彈性,把繼承類別改成實作介面能讓物件可以有更多能力。


定義函式時應該盡量避免帶有副作用(side effect)。在這裡副作用指的是函式會對「環境」造成影響或是被影響,也就是對這個函式的呼叫會執行某種操作,而不只是進行某種計算。常見的副作用包含存取外部變數、印出字串或讀取使用者的輸入、或是與外部設備的互動。副作用會產生隱性的依賴,使得看似無關的程式碼片段隔空互動,這會讓重構程式碼變得困難。就算有辦法明確地判斷函式是否有哪些副作用,也應該盡量避免。除非你明確知道這些函式是怎麼以副作用互動的,一般來說是不可能進一步重構程式碼的。例如當你想要把loop用iterator pattern重構,如果裡面的操作包含大量的變數修改,將會難以將它們分解成個別的map操作。例外機制也是類似的情況。有些程式語言的函式預設會丟出例外,而且還沒有辦法知道它會丟哪些例外。這使得例外處理變得非常麻煩,你必須祈禱說明文件有這部分的描述。就算有也不一定準確,如果它使用你提供的函式(例如comparator),但這個函式丟出了例外,這時會怎麼樣?就算Java要求必須明確地標示函式會丟出哪些例外,也應該少用例外機制。問題在於呼叫函式時預設不會處理例外,從語法上無法區別這時有沒有例外會被拋出,當除錯時發現沒執行到某行程式碼是因為前面呼叫的函式拋出例外,這常常讓人感到被背叛。比較好的做法應該是回傳執行狀態,讓使用者明確地判斷狀態後再進行下一步操作。這種做法比起例外機制繁雜一點,常常需要對每個函式都進行個別例外處理。然而例外機制的「預設正確」的行為就像是null pointer的問題,這只是透過掩蓋問題來簡化邏輯。有些程式語言甚至會濫用例外機制來控制流程,例如Python可以透過StopIteration, GeneratorExit跳出generator的程序,然而這根本不是例外,官方文件也說明它技術上不是例外。例外機制不應被當作能回傳多種狀態的方法而濫用。

留言
avatar-img
留言分享你的想法!
avatar-img
have bear的沙龍
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
have bear的沙龍的其他內容
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
看更多
你可能也想看
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
全球科技產業的焦點,AKA 全村的希望 NVIDIA,於五月底正式發布了他們在今年 2025 第一季的財報 (輝達內部財務年度為 2026 Q1,實際日曆期間為今年二到四月),交出了打敗了市場預期的成績單。然而,在銷售持續高速成長的同時,川普政府加大對於中國的晶片管制......
Thumbnail
全球科技產業的焦點,AKA 全村的希望 NVIDIA,於五月底正式發布了他們在今年 2025 第一季的財報 (輝達內部財務年度為 2026 Q1,實際日曆期間為今年二到四月),交出了打敗了市場預期的成績單。然而,在銷售持續高速成長的同時,川普政府加大對於中國的晶片管制......
Thumbnail
重點摘要: 6 月繼續維持基準利率不變,強調維持高利率主因為關稅 點陣圖表現略為鷹派,收斂 2026、2027 年降息預期 SEP 連續 2 季下修 GDP、上修通膨預測值 --- 1.繼續維持利率不變,強調需要維持高利率是因為關稅: 聯準會 (Fed) 召開 6 月利率會議
Thumbnail
重點摘要: 6 月繼續維持基準利率不變,強調維持高利率主因為關稅 點陣圖表現略為鷹派,收斂 2026、2027 年降息預期 SEP 連續 2 季下修 GDP、上修通膨預測值 --- 1.繼續維持利率不變,強調需要維持高利率是因為關稅: 聯準會 (Fed) 召開 6 月利率會議
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 數學中函數概念的重要性難以盡書,亦很難想像沒有函數概念的數學可以走多遠。誇張一點,我們可以說很大部份的數學都是按函數概念操作的。但少有人留意到,在某個意義上,函數可說是數學語言的一個語構處理。 漢語「函數」一詞乃
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 數學中函數概念的重要性難以盡書,亦很難想像沒有函數概念的數學可以走多遠。誇張一點,我們可以說很大部份的數學都是按函數概念操作的。但少有人留意到,在某個意義上,函數可說是數學語言的一個語構處理。 漢語「函數」一詞乃
Thumbnail
這一篇會介紹非常重要的 JavaScript 函式概念 - 高階函式(Higher-order function),高階函數是將一個或多個函數作為參數,或將一個函數作為結果返回的函數。在本文中,我們將深入探討什麽是高階函數、使用高階函數的好處以及如何在實際應用中使用高階函數,函式導向是什麼?
Thumbnail
這一篇會介紹非常重要的 JavaScript 函式概念 - 高階函式(Higher-order function),高階函數是將一個或多個函數作為參數,或將一個函數作為結果返回的函數。在本文中,我們將深入探討什麽是高階函數、使用高階函數的好處以及如何在實際應用中使用高階函數,函式導向是什麼?
Thumbnail
你會在程式裡面寫函數嗎? 通常寫函數的第一個問題,就是要給函數取名字。 名字取得不好,後來調用函數不自然,就會拖垮整個寫程式的效率。 為函數命名,也是一門技術,好的函數命名,就能提高函數被重複使用的頻率。 然而,也是在某些情況下,我們需要「一次性函數」。 沒錯,用完即丟的函數。
Thumbnail
你會在程式裡面寫函數嗎? 通常寫函數的第一個問題,就是要給函數取名字。 名字取得不好,後來調用函數不自然,就會拖垮整個寫程式的效率。 為函數命名,也是一門技術,好的函數命名,就能提高函數被重複使用的頻率。 然而,也是在某些情況下,我們需要「一次性函數」。 沒錯,用完即丟的函數。
Thumbnail
  程式中很常會看到千奇百怪的運算式,這些運算式都隱藏著各種運算元和運算子,這些是什麼呢?讓我們來一探究竟。   運算元是指變數、常數這類(如:A、B、C、Data、123等),運算子是指運算符號(如:+、-、*、/、%、==、<、&&等這類型),這邊就要介紹C#的運算子以及怎麼使用。
Thumbnail
  程式中很常會看到千奇百怪的運算式,這些運算式都隱藏著各種運算元和運算子,這些是什麼呢?讓我們來一探究竟。   運算元是指變數、常數這類(如:A、B、C、Data、123等),運算子是指運算符號(如:+、-、*、/、%、==、<、&&等這類型),這邊就要介紹C#的運算子以及怎麼使用。
Thumbnail
這篇文章將會介紹函式(Function)及其回傳值(retrun)的定義及介紹。
Thumbnail
這篇文章將會介紹函式(Function)及其回傳值(retrun)的定義及介紹。
Thumbnail
這篇文章為介紹C#基礎知識的一部分,如果你是直接開始寫程式的C#程式員,可以看看這篇文章補足一些基礎知識。
Thumbnail
這篇文章為介紹C#基礎知識的一部分,如果你是直接開始寫程式的C#程式員,可以看看這篇文章補足一些基礎知識。
Thumbnail
雜湊演算法(hash function)。或許你聽過它,但你是否了解它?劍術大師都說要人劍合一了,若是資訊人員不能人與技術合一,那要如何登峰造極?我們必須正確的使用它,才能讓它變成你的武器。 縮圖來源:https://www.pexels.com/zh-tw/photo/53207/
Thumbnail
雜湊演算法(hash function)。或許你聽過它,但你是否了解它?劍術大師都說要人劍合一了,若是資訊人員不能人與技術合一,那要如何登峰造極?我們必須正確的使用它,才能讓它變成你的武器。 縮圖來源:https://www.pexels.com/zh-tw/photo/53207/
Thumbnail
函式(Function)、傳值法、傳位址法、傳參考法
Thumbnail
函式(Function)、傳值法、傳位址法、傳參考法
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News