函式的類型

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

函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如std::function<int(int)> twice(std::function<int(int)>)。rust的函式類型寫法回傳值被放在後面,這更符合閱讀習慣,例如 fn(f32, f32) -> i32,但它其實只能表達靜態函式,在這裡我們先暫時忘記這部分的差異。

能接受或是回傳函式的函式稱作高階函式,因為它把函式作為和資料結構同等的存在。之前提到過函式的參數列表可看作乘法類型,例如fn(f32, f32) -> i32可看作是接受(f32,f32)作為參數並回傳i32的函式,在函數式程式語言裡常常用currying寫成fn(f32) -> fn(f32) -> i32,也就是回傳接受第二個參數的函式,等到所有參數都到位了才會回傳i32(也有可能是先做部分運算,再回傳剩下運算的函式)。他回傳的函式帶有第一個參數的資訊,這被稱做閉包。這兩種寫法對嚴格控制副作用的程式語言來說是沒有區別的,因為從類型上它並沒有任何副作用,因此何時運算並不會影響回傳結果。

函式可看作帶有佔位符的表達式範本,就像printf的格式化字串一樣,呼叫函式就像是把範本裡的佔位符取代成給定的參數。但如果某個參數出現在範本裡的兩個地方,傳入此參數的表達式就必須複製成多個,當這個表達式做了修改變數或是印出文字等操作,這種取代就會使行為改變。然而在像是Haskell等純函數式程式語言中,這不只是像是而已,這種取代是完全沒有問題的,唯一的不同只有效能上的差異。這種特性稱為引用透明性(referential transparency):我們可以自由地把函式的呼叫直接改寫成它的定義並取代相應的參數,就像數學函數一樣。具有引用透明性的函式必須沒有副作用,否則結果就可能會改變。這代表這個函式回傳的結果只依賴於參數,任何情境的改變都不會影響計算結果。這使得重構程式碼變得簡單,我們不再需要考慮執行順序、呼叫次數或是呼叫時機,它只會受到傳入參數的束縛,程式邏輯因而變得更容易理解。因此函式應該寫得盡量地引用透明,這也是函式應該盡量避免使用副作用的原因。

如果說函式的參數代表表達式範本上的「洞」,參數類型就是代表洞的形狀。構成加法類型與乘法類型的成員類型角色是同等的,而構成函式類型的參數類型與回傳類型角色則是不一樣的。加法/乘法類型的成員類型和函式的回傳類型代表它能「提供」這種數值,而函式的參數類型則代表它「需要」這種數值。物件導向的繼承機制使我們可以把物件實體當作更抽象的父類別物件來操作,例如List類別的物件可以當作Collection類別的物件。而乘法/加法類型的成員也可以這麼做,例如Tuple<List, Integer>類型的物件可以當作Tuple<Collection, Number>類型的物件,因為它被當作是前者的父類別。而函式的參數類型則相反,例如fn(Collection)->Integer的函式可以當作fn(List)->Number的函式使用,而非fn(Object)->Number。如果類型參數的子類關係與複合類型的子類關係同向則稱為協變(covariant),反向則稱為逆變(contravariant),這和線性代數裡相同名詞的概念有類似的意義。協變的位置代表它會提供這類型的資料(producer),而逆變的位置則是它會需要這個類型的資料(consumer)。有趣的是它有負負得正的特性,例如函式類型fn(fn(res)->bool)->bool,其中fn(res)->bool位於逆變的位置,而res相對於最外層的函式則是協變的:這個函式會產生出res並喂給傳入的callback函式,因此它是res的producer。

在rust,類型描述的是在記憶體中的實際結構,而函式實際上就是一段指令,因此它也可以用類型描述。跟其他語言不一樣的是,即使兩個函式的參數類型和回傳類型都一樣,它們的類型還是不一樣,因為指令長度和結構可能不一樣。closure和async等特殊結構更是如此,編譯器會在內部實作一個結構保存捕捉的變數和管理狀態,因此它們實際上就是一種資料結構。它們都擁有匿名的類型,每個函式、閉包都有獨立的類型,並沒辦法直接混用(see Rust Functions Are Weird (But Be Glad))。基於這個理由,rust主要是以trait描述函式的signature,而非類型。而對於函數式編程來說則沒有這麼具體,函式的類型是由參數類型和回傳類型組成,不管它內部是什麼結構,只要這兩項相同就是同一種類型。函數式程式語言的類型描述的是更抽象的結構和行為,並不考慮物理上系統怎麼做處理。前幾篇文章以「函數式編程的類型描述的是實際的資料結構」用來對比「物件導向的變數類型與實體類型可以不一樣」,事實上這是不準確的,尤其是討論到函式類型時(對於惰性求值的Haskell更為明顯)。正確來說,函數式編程的類型描述的是「抽象的結構」。這跟物件導向的差別在於它只會包含它描述的特性,不會像物件導向的子類轉換使得變數實際上帶有其他特性。例如在TypeScript裡{x:1, y:2, z:3}可以被當作{x:number, y:number}類型下的物件,但它多了可存取z成員的特性,這對於函數式編程的類型系統是非法的。

乘法類型和加法類型的「資料的抽象結構」可以用一個函式描述。乘法類型具有「同時取得成員」的特性,例如類型type IntAndStr = Tuple<int, string>可以由fn(fn(int) -> fn(string) -> A) -> A描述,其中我們透過callback函式取得它的成員。加法類型具有「取得其中一個成員」的特性,例如類型type IntOrStr = Result<int, string>可由fn(fn(int) -> A) -> fn(fn(string) -> A) -> A描述,其中我們透過兩個callback函式分別取得它的成員。如果嚴格遵守函數式編程的教條,只有唯一一種方法可以實作出這兩類函式,實際上就只是pattern matching而已,而它們顯現出乘法類型和加法類型的基礎且唯一的特性,因此可以說它們是代數資料類型的另一種實現方法,這被稱作Scott encoding。事實上所有代數資料類型,包括還沒介紹到的遞迴類型,都能用函式描述。相對於圖靈機使用資料描述函式,這是稱為lambda calculus的另一套計算理論。smalltalk同樣利用方法定義物件本身,例如boolean只是擁有if-else函式的閉包,然而物件會有內部狀態,這使得這種閉包與純函數式的閉包變成完全不一樣的東西。

函式類型也是代數資料類型的一種,他擁有類似指數的代數結構:fn(A)->B類似於B^A,例如fn(A)->fn(B)->C跟fn(A,B)->C是等價的,因為(C^B)^A = C^(A*B);(fn(A)->C, fn(B)->C)和fn(Result<A, B>)->C是等價的,因為(C^A)*(C^B) = C^(A+B)。它還有一個特殊的結構:fn<B>(fn(A)->B)->B相當於A,其中它擁有泛型變數B。透過這些結構可以很容易推導出Scott encoding。它在實體數量上也符合這個代數結構,例如擁有fn(bool)->i8類型的函式有256^2種;可以想像它其實是(i8,i8),傳入false時回傳第一個,傳入true時回傳第二個。這些代數結構只有在沒有副作用時才符合,基於這點,純函數式程式語言對於函式的理解和其他語言有根本上的不同。沒有副作用的函式根本上與資料結構無異,例如Map<A,B>就只是能根據A取得B的資料結構,它可以直接改寫成fn(A)->Option<B>。如果再包含惰性求值的特性,甚至反過來也是可以的:函式fn(A)->B可以改寫成惰性求值的Map<A,B>,這種方法可以達到暫存計算結果的效果。

留言
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
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
從學生時期一直到現在,韓國的明星一直都是潮流前端的話題。 這次的分享文是...第幾篇?
Thumbnail
從學生時期一直到現在,韓國的明星一直都是潮流前端的話題。 這次的分享文是...第幾篇?
Thumbnail
珍妮特麥考迪在其自傳《我很高興我媽死了》中,深入探討了其童星生涯及與母親複雜的關係。書中描述了在演藝圈中經歷的情感虐待,以及母親的控制慾如何影響其成長。這本書不僅揭露了演藝圈的黑暗面,更探索了珍妮特如何從痛苦中走出來,成為一名堅強的倖存者。
Thumbnail
珍妮特麥考迪在其自傳《我很高興我媽死了》中,深入探討了其童星生涯及與母親複雜的關係。書中描述了在演藝圈中經歷的情感虐待,以及母親的控制慾如何影響其成長。這本書不僅揭露了演藝圈的黑暗面,更探索了珍妮特如何從痛苦中走出來,成為一名堅強的倖存者。
Thumbnail
我們在上一篇簡單介紹了 int(整數)是做什麼用的,接下來要介紹常和他一起出現的好朋友 float 浮點數 跟 str 字串。 float 浮點數: 函數的式子寫做 float( ) ,浮點數就是帶有小數點的資料型別,他可以將字串或是數字轉換為有小數點的狀態。前提是字串內的字符必須是數字的格
Thumbnail
我們在上一篇簡單介紹了 int(整數)是做什麼用的,接下來要介紹常和他一起出現的好朋友 float 浮點數 跟 str 字串。 float 浮點數: 函數的式子寫做 float( ) ,浮點數就是帶有小數點的資料型別,他可以將字串或是數字轉換為有小數點的狀態。前提是字串內的字符必須是數字的格
Thumbnail
在這一章中,我們探討了 PHP 中的函數,包括函數的基本結構、不同的函數定義方式(如函數聲明、函數表達式、箭頭函數和匿名函數)以及如何呼叫函數。我們還討論了函數的參數處理方式,包括單個參數、多個參數、預設參數值和剩餘參數。此外,我們還介紹了函數的返回值,包括返回單個值、返回物件和返回函數的情況。
Thumbnail
在這一章中,我們探討了 PHP 中的函數,包括函數的基本結構、不同的函數定義方式(如函數聲明、函數表達式、箭頭函數和匿名函數)以及如何呼叫函數。我們還討論了函數的參數處理方式,包括單個參數、多個參數、預設參數值和剩餘參數。此外,我們還介紹了函數的返回值,包括返回單個值、返回物件和返回函數的情況。
Thumbnail
本章節的目的是介紹 Kotlin 的各種資料型別。包括內建型別如基本數值型別、字串型別和布林型別等,以及如何進行型別轉換。此外,也介紹了如何定義自訂型別(類)和元組型別,以及 Kotlin 提供的集合型別,例如列表(List)、集合(Set)和映射(Map)以及陣列(Array)。
Thumbnail
本章節的目的是介紹 Kotlin 的各種資料型別。包括內建型別如基本數值型別、字串型別和布林型別等,以及如何進行型別轉換。此外,也介紹了如何定義自訂型別(類)和元組型別,以及 Kotlin 提供的集合型別,例如列表(List)、集合(Set)和映射(Map)以及陣列(Array)。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News