函式的類型

更新於 發佈於 閱讀時間約 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
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(pr
物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇
在現代,物件導向雖然仍是主流,函數式慢慢得到關注。物件導向並不適合所有的程式邏輯,但在像是Java的物件導向的程式語言中,幾乎所有值都被當作物件,因此在一些情境下Java寫起來會非常冗余。物件導向流行的原因大概是因為它的思考方式比較符合我們對於世界的認知,但邏輯推理與解決問題的方式卻不一定符合我們的
前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(pr
物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇
在現代,物件導向雖然仍是主流,函數式慢慢得到關注。物件導向並不適合所有的程式邏輯,但在像是Java的物件導向的程式語言中,幾乎所有值都被當作物件,因此在一些情境下Java寫起來會非常冗余。物件導向流行的原因大概是因為它的思考方式比較符合我們對於世界的認知,但邏輯推理與解決問題的方式卻不一定符合我們的
你可能也想看
Google News 追蹤
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
從學生時期一直到現在,韓國的明星一直都是潮流前端的話題。 這次的分享文是...第幾篇?
Thumbnail
珍妮特麥考迪在其自傳《我很高興我媽死了》中,深入探討了其童星生涯及與母親複雜的關係。書中描述了在演藝圈中經歷的情感虐待,以及母親的控制慾如何影響其成長。這本書不僅揭露了演藝圈的黑暗面,更探索了珍妮特如何從痛苦中走出來,成為一名堅強的倖存者。
Thumbnail
我們在上一篇簡單介紹了 int(整數)是做什麼用的,接下來要介紹常和他一起出現的好朋友 float 浮點數 跟 str 字串。 float 浮點數: 函數的式子寫做 float( ) ,浮點數就是帶有小數點的資料型別,他可以將字串或是數字轉換為有小數點的狀態。前提是字串內的字符必須是數字的格
主要來講宣告函式跟箭頭函式 : 宣告函式(Function Declaration) 語法: function functionName(parameters) { return result; } 特點: 使用 function 關鍵字 函式名稱是必需的 存在函式
Thumbnail
在這一章中,我們探討了 PHP 中的函數,包括函數的基本結構、不同的函數定義方式(如函數聲明、函數表達式、箭頭函數和匿名函數)以及如何呼叫函數。我們還討論了函數的參數處理方式,包括單個參數、多個參數、預設參數值和剩餘參數。此外,我們還介紹了函數的返回值,包括返回單個值、返回物件和返回函數的情況。
Thumbnail
本章節的目的是介紹 Kotlin 的各種資料型別。包括內建型別如基本數值型別、字串型別和布林型別等,以及如何進行型別轉換。此外,也介紹了如何定義自訂型別(類)和元組型別,以及 Kotlin 提供的集合型別,例如列表(List)、集合(Set)和映射(Map)以及陣列(Array)。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
韓國政府否決《特別法》,導致梨泰院慘案的真相調查、受害者支援等問題持續發酵。政府憂憲法價值侵害、財源浪費,遭遇在野黨批評。遺屬不滿未獲支援,社會輿論趨於抨擊。
Thumbnail
倘若是有追過韓劇《信號Signal》和電影《殺人回憶》的小夥伴,相信對於“華城連續殺人命案”這個驚悚的社會案件肯定不陌生。 嫌犯犯案的期間落於西元1986-1991年。 在那個民風淳樸且保守的年代,發生了如此轟動的案件,就連警方也無法在短時間內抓到歹徒,難免人心惶惶。 由於當時沒有
函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如st
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
從學生時期一直到現在,韓國的明星一直都是潮流前端的話題。 這次的分享文是...第幾篇?
Thumbnail
珍妮特麥考迪在其自傳《我很高興我媽死了》中,深入探討了其童星生涯及與母親複雜的關係。書中描述了在演藝圈中經歷的情感虐待,以及母親的控制慾如何影響其成長。這本書不僅揭露了演藝圈的黑暗面,更探索了珍妮特如何從痛苦中走出來,成為一名堅強的倖存者。
Thumbnail
我們在上一篇簡單介紹了 int(整數)是做什麼用的,接下來要介紹常和他一起出現的好朋友 float 浮點數 跟 str 字串。 float 浮點數: 函數的式子寫做 float( ) ,浮點數就是帶有小數點的資料型別,他可以將字串或是數字轉換為有小數點的狀態。前提是字串內的字符必須是數字的格
主要來講宣告函式跟箭頭函式 : 宣告函式(Function Declaration) 語法: function functionName(parameters) { return result; } 特點: 使用 function 關鍵字 函式名稱是必需的 存在函式
Thumbnail
在這一章中,我們探討了 PHP 中的函數,包括函數的基本結構、不同的函數定義方式(如函數聲明、函數表達式、箭頭函數和匿名函數)以及如何呼叫函數。我們還討論了函數的參數處理方式,包括單個參數、多個參數、預設參數值和剩餘參數。此外,我們還介紹了函數的返回值,包括返回單個值、返回物件和返回函數的情況。
Thumbnail
本章節的目的是介紹 Kotlin 的各種資料型別。包括內建型別如基本數值型別、字串型別和布林型別等,以及如何進行型別轉換。此外,也介紹了如何定義自訂型別(類)和元組型別,以及 Kotlin 提供的集合型別,例如列表(List)、集合(Set)和映射(Map)以及陣列(Array)。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
韓國政府否決《特別法》,導致梨泰院慘案的真相調查、受害者支援等問題持續發酵。政府憂憲法價值侵害、財源浪費,遭遇在野黨批評。遺屬不滿未獲支援,社會輿論趨於抨擊。
Thumbnail
倘若是有追過韓劇《信號Signal》和電影《殺人回憶》的小夥伴,相信對於“華城連續殺人命案”這個驚悚的社會案件肯定不陌生。 嫌犯犯案的期間落於西元1986-1991年。 在那個民風淳樸且保守的年代,發生了如此轟動的案件,就連警方也無法在短時間內抓到歹徒,難免人心惶惶。 由於當時沒有
函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如st