函式的正確用法

更新於 發佈於 閱讀時間約 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
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇
在現代,物件導向雖然仍是主流,函數式慢慢得到關注。物件導向並不適合所有的程式邏輯,但在像是Java的物件導向的程式語言中,幾乎所有值都被當作物件,因此在一些情境下Java寫起來會非常冗余。物件導向流行的原因大概是因為它的思考方式比較符合我們對於世界的認知,但邏輯推理與解決問題的方式卻不一定符合我們的
物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇
在現代,物件導向雖然仍是主流,函數式慢慢得到關注。物件導向並不適合所有的程式邏輯,但在像是Java的物件導向的程式語言中,幾乎所有值都被當作物件,因此在一些情境下Java寫起來會非常冗余。物件導向流行的原因大概是因為它的思考方式比較符合我們對於世界的認知,但邏輯推理與解決問題的方式卻不一定符合我們的
你可能也想看
Google News 追蹤
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
4.0 尚芬哥爾操作與升型處理 4.1 尚芬哥爾操作 二 4.1_1 是一個函數。在缺乏規約的情況下, 是有歧義的,因為在實際操作上,我們不知道應該如何使用函子 F。 使用括號是一個常用的手段,例如 這明顯是一個二元函數,換句話說,這個函數的返回值同時需要兩個輸入值。假設 F 是求和函數
Thumbnail
本文探討Python中函式的定義和作用,重點解釋如何通過函式定義def 來解決重複程式碼的問題,並介紹函式引數的預設值和可變引數的使用。並分析了函式的命名衝突問題,以及函式的作用域管理,特別是全域性和區域性變數的區別。這篇文章將幫助初學者更好地理解Python函式的基礎知識,提升程式碼質量。
Thumbnail
今天要來介紹的是Python中資料型別的函數, 這幾天學習的素材是Youtube上“程式柴大大的Python 6 小時初學者課程”,一步一步帶著大家操作並解,學習中也別忘了要多多練習,練習的部分我是把我學到的東西請Chatgpt幫我出類似的題型並讓我練習。 以下我先寫出一個簡單的code,再加以
Thumbnail
1.0 從函數到函算語法 1.4 函算語法 1.4.1 語法範疇理論導論 1.4.2 函算語法與函數概念 三 弗雷格從語言結構的觀點出發,提出了函數可以被視為一個不完整的表式。如果我們將一個函數拆解為一個由一個函子及其 (一個或多個) 論元所組成的表式,那麼該函子便是一個有待滿足的
Thumbnail
本章節主要介紹Java語言中的函數(也稱為方法)的使用,包括函數的基本結構、函數表達式(Lambda表達式)、箭頭函數、匿名函數的使用,以及如何呼叫函數、如何使用函數參數和函數的返回值等內容。通過學習本章節,讀者將能夠熟練掌握Java語言中的函數相關知識,並能夠在實際編程中靈活運用。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 數學中函數概念的重要性難以盡書,亦很難想像沒有函數概念的數學可以走多遠。誇張一點,我們可以說很大部份的數學都是按函數概念操作的。但少有人留意到,在某個意義上,函數可說是數學語言的一個語構處理。 漢語「函數」一詞乃
Thumbnail
本章節旨在介紹 C# 中函數的基本結構,包括訪問修飾符、返回類型、方法名稱、參數列表和方法體。同時,也介紹了函數的各種呼叫方式、參數傳遞方式和返回值類型。讀者可以通過本章節,深入理解 C# 中函數的使用和應用。
Thumbnail
今天來介紹python的函式 函式在python中是非常重要的一環,因為到了後期,程式會越來越複雜。 而函式可以想成是容易管理的小程式,當我們需要使用時,只需呼叫即可。
Thumbnail
本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
4.0 尚芬哥爾操作與升型處理 4.1 尚芬哥爾操作 二 4.1_1 是一個函數。在缺乏規約的情況下, 是有歧義的,因為在實際操作上,我們不知道應該如何使用函子 F。 使用括號是一個常用的手段,例如 這明顯是一個二元函數,換句話說,這個函數的返回值同時需要兩個輸入值。假設 F 是求和函數
Thumbnail
本文探討Python中函式的定義和作用,重點解釋如何通過函式定義def 來解決重複程式碼的問題,並介紹函式引數的預設值和可變引數的使用。並分析了函式的命名衝突問題,以及函式的作用域管理,特別是全域性和區域性變數的區別。這篇文章將幫助初學者更好地理解Python函式的基礎知識,提升程式碼質量。
Thumbnail
今天要來介紹的是Python中資料型別的函數, 這幾天學習的素材是Youtube上“程式柴大大的Python 6 小時初學者課程”,一步一步帶著大家操作並解,學習中也別忘了要多多練習,練習的部分我是把我學到的東西請Chatgpt幫我出類似的題型並讓我練習。 以下我先寫出一個簡單的code,再加以
Thumbnail
1.0 從函數到函算語法 1.4 函算語法 1.4.1 語法範疇理論導論 1.4.2 函算語法與函數概念 三 弗雷格從語言結構的觀點出發,提出了函數可以被視為一個不完整的表式。如果我們將一個函數拆解為一個由一個函子及其 (一個或多個) 論元所組成的表式,那麼該函子便是一個有待滿足的
Thumbnail
本章節主要介紹Java語言中的函數(也稱為方法)的使用,包括函數的基本結構、函數表達式(Lambda表達式)、箭頭函數、匿名函數的使用,以及如何呼叫函數、如何使用函數參數和函數的返回值等內容。通過學習本章節,讀者將能夠熟練掌握Java語言中的函數相關知識,並能夠在實際編程中靈活運用。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 數學中函數概念的重要性難以盡書,亦很難想像沒有函數概念的數學可以走多遠。誇張一點,我們可以說很大部份的數學都是按函數概念操作的。但少有人留意到,在某個意義上,函數可說是數學語言的一個語構處理。 漢語「函數」一詞乃
Thumbnail
本章節旨在介紹 C# 中函數的基本結構,包括訪問修飾符、返回類型、方法名稱、參數列表和方法體。同時,也介紹了函數的各種呼叫方式、參數傳遞方式和返回值類型。讀者可以通過本章節,深入理解 C# 中函數的使用和應用。
Thumbnail
今天來介紹python的函式 函式在python中是非常重要的一環,因為到了後期,程式會越來越複雜。 而函式可以想成是容易管理的小程式,當我們需要使用時,只需呼叫即可。
Thumbnail
本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。