函數是一種對應關係,所以在討論函數之前,我們要先了解什麼對應關係。
對應關係就是兩個集合之間的關係,一個集合為定義域,另一個集合則是對應域,定義域的每個元素可能對應到0個、1個或多個對應域裏的元素,嚴謹一點,可以用有序對(x,y)來表示,其中 x 是定義域的元素, y 是對應域的元素。。
國高中數學討論的集合主要是實數,我們這裏就以實數為例,討論對應關係。通常常見的方程式、不等式都是一種對應關係。
例如: f(x) = x²,就是一個對應關係,我們必須先設定那一個變數代表定義域的元素,那一個變數代表對應域的元素。通常我們預設x是定義域裏的元素,也稱為因變數,y是對應域的元素,稱為應變數, x 的每一個值都對應到 y 的一個值,例如: 1 → 1, 2 → 4,3 → 9, 4 → 16 ,…。反過來, y 的每一個值都對應到 x 的一個值,例如: 1 → 1, 1 → -1,4 → 2, 4 → -2 ,…
再看不等式 3x ≥ y ,這個不等式也是一種對應關係。
函數是兩個集合的一種對應關係,一個集合是定義域,一個集合是對應域;定義義域中的每一個元素,在對應域中都有「唯一」的元素與之對應。並不是所有的對應關係都是函數,只有滿足「唯一」的對應關係才是函數,我們將這種對應關係記作 x → y。
假設我們的定義域和對應域都是實數,以平方關係為例,每一個實數都可以計算出它的平方,例如: 1 → 1, 2 → 4,3 → 9, 4 → 16 ,…,這種對應關係就是函數,我們將這種函數記作 f: x → x² ,或是寫作 f(x) = x²。
國高中求學時期,許多的參考書或考卷常常用語不清楚,導致學生對函數的定義感到困惑。例如說 y²=x,請問 y 是不是 x 的函數,
函數是一個很難望文生義的數學名詞,所以我們從英文定義更容易擷取它的內涵,函數的英文是function,這個字的意思是「功能」,一般可以從下圖理解成輸入與輸出的關係,也就是輸入是定義域,輸出是對應域。而 f(x) = x² 這種寫法容易讓人將函數理解為公式,而無法體會函數其實是兩個集合的對應關係。 f(x) 在英文中讀作「f of x」,大多數人翻譯成「f在x上的值」,但我覺得這不是一個好的唸法。我通常會念成「x的f」,強調f是一種對應關係。闢如說,子父關係,我們會說「張三的父親是張丙」來強調兒子父親是一種對應,由每個人恰有一個父親,所以兒子對應父親是一種函數關係,所以「張三的父親」對應的是一個確切的人,沒有模糊性,所以後面我們可以接「是」這個字。反過來說,兒子的關係就不是函數關係,因為一個人可能有多個兒子或是沒有兒子,所以「張丙的兒子」不是一個確切的人,所以後面不能接「是」。
國高中的數學,我們討論的定義域和對應域通常都是實數,所以當函數關係成立的時候,我們就可以說 f(x) = x² 這種寫法,因為這個對應具有唯一性,所以可以使用等號。而反過來,當一個關係不是函數關係的時候,我們就不能用等號,例如,平方根的關係,因為一個正數有兩個平方根,而負數沒有平方根,但是你會很疑惑,我們不是有 這種寫法嗎?事實上,這個
取的是正平方根,如此就會只有一個答案,也就滿足函數的定義。
事實上,函數的定義域和對應域不一定要是實數,也可以是各式各樣的集合,也可以是函數自己形成的集合,這些我們在後面都有機會可以碰到。
在typescript中,函數是一級函數,也就是說函數和number,string和boolean…等其它型別的值一樣,可以指定給變數,也可以當作參數傳遞給另一個函數,也可以從函數中回傳函數。這種特性實現了數學上將函數抽象化,讓它可以成為集合中的元素,在函數式程式設計中非常有用,也是函數式程式設計中函數必須具備的特性,也是High Order Function(HOF)設計的基礎條件。
Typescript函式的寫法有下列幾種:
function 函數名稱(參數1: 型別1, 參數2: 型別2, ...): 回傳型別 {
函數內容
return 回傳值
}
這種寫法是函數的標準寫法,函數名稱是函數的識別字,參數是函數的輸入,回傳型別是函數的輸出,函數內容是函數的邏輯,return是函數的輸出,回傳值是函數的輸出值。這種寫法也具備Hoisting的特性,也就是函數的宣告可以放在函數的呼叫之前。
例如:
function add(a: number, b: number): number {
return a + b;
}
上面這個add函數的函數名稱add會有一個屬性叫做name,而它的值就是'add',對照後面的匿名函數,這是一個非匿名函數。
const 變數名稱 = function(參數1: 型別1, 參數2: 型別2, ...): 回傳型別 {
函數內容
return 回傳值
}
這種寫法是將一個匿名函數指定給一個變數,相較於非匿名函數,這種寫法並不具備Hoisting的特性,也就是函數的宣告不能放在函數的呼叫之前, 其它方面並沒有太大的差異。
例如:
const add = function(a: number, b: number): number {
return a + b;
}
雖然我們這種函數是一種匿名函數,但是許多的Javascript引擎仍然給add一個name的屬性,值是'add',在特別的寫法中,可以發現它們其實沒有name的屬性。詳細請參考Mastering_JavaScript_Functional_Programming這本書第2章。
const 變數名稱 = (參數1: 型別1, 參數2: 型別2, ...): 回傳型別 => {
函數內容
return 回傳值
}
或
const 變數名稱 = (參數1: 型別1, 參數2: 型別2, ...): 回傳型別 => 回傳值
回傳值可以是表達式,也可以是函數呼叫。
表達式是一種可以計算出值的式子,例如:1 + 2,這個式子可以計算出3這個值,表達式可以被它所計算出來的值所取代,例如:1 + 2可以被3所取代。
const add = (a: number, b: number): number => a + b;
這種寫法的箭頭函數最合乎我們數學對應關係的寫法,因為我們在數學中,函數的寫法是
f: x → x²,這種寫法非常接近我們的寫法,所以這種寫法也稱為lambda寫法;箭頭函數也是匿名函數,所以也不具備Hoisting的特性,也就是函數的宣告不能放在函數的呼叫之前。
在函數裏面使用if-else語法回傳函數值,if和else的程式區段都要回傳滿足回傳型態的值,如此才能滿足函數定義的要求。
// 合乎定義的函數
const absolute = (x: number) => {
if (x >= 0) return x
else return -x
}
// 不合乎定義的函式,x < 0時無回傳值
const absolute1 = (x: number) => {
if (x >= 0) return x
}
三元運算子(Ternary Operator)也是一種表達式,可以用來替代一些if-else語法,它的寫法是條件式 ? 值1 : 值2,如果條件式成立,則回傳值1,否則回傳值2。例如:1 > 2 ? 3 : 4,這個式子可以計算出4這個值,因為1 > 2不成立,所以回傳4。
Typescript中,函數也是一種型別,例如輸入型別是number,輸出型別是number的函數型別可以寫成(number) => number,這種寫法稱為函數型別,因此函數在定義的時候,可以用type關鍵子定義函數的型別,再將函數宣告為此函數型別,如此便不必在函數的內容中寫出參數的型別和回傳值的型別。以箭頭函數為例,我們可以這樣寫:
type 函數型別名稱 = (參數1: 型別1, 參數2: 型別2, ...) => 回傳型別
const 變數名稱: 函數型別名稱 = (參數1, 參數2, ...) => 回傳值
例如:
type Add = (a: number, b: number) => number;
const add: Add = (a, b) => a + b;
引用透明性(Referential Transparency)是指一個表達式可以被它所計算出來的值所取代,而不影響程式的結果。例如:1 + 2可以被3所取代,而不影響程式的結果。這種特性在函數式程式設計中非常重要,因為函數式程式設計中函數必須具備的特性。
所有的數學算式和數學函數都具有引用透明性,例如: f(x) = x²,我們可以將 x 替換為任何數字,例如: f(1) = 1² = 1, f(2) = 2² = 4, f(3) = 3² = 9, f(4) = 4² = 16,…。
當一個函數具備引用透明性,便是代表相同的輸入,必定會得到相同的輸出,在程式設計中,我們稱這種函數為純函數,也就是符合數學規定的函數。
和日期時間有關的函數以及亂數函數通常不具有引用透明性,也就是相同的輸出可能會得到不同的輸出,例如:
const now = new Date();
const random = Math.random();
now和random這兩個函數每次都會得到不同的結果,因此不是純函數。
早期的程式語言通常會區分Function和Subroutine,Function會有回傳值,而Subroutine不會有回傳值,例如:VB、Fortran和Pascal都有明顯區分(Pascal使用Procedure),很明顯Subroutine沒有回傳值,主要的任務便是進行一輸出入的操作,或是副程式外部資料結構的處理演算,因此Function的定義也比較接近數學上的函數(因為一定要有回傳值),但是並沒有嚴格禁止操作外部變數。從C語言開始,似乎這兩個就合而為一,不再區分,是否回傳值也沒有硬性規定。C、C++和Python的關鍵字上並沒有使有Function這個字,許多教學用書籍都通用函數(Function)這個名詞,因此數學上和程式語言上函數(Function)的混用,讓這個詞語的意義存在混洧的情形。
而函數式程式設計便將程式設計中符合數學定義的函數稱純函數(Pure Function),而不滿足數學定義的函數程式稱為有副作用(Side Effect)的函數。而像Haskell這種純粹為函數式程式設計發展的語言便嚴格規定只能使用純函數,也為了只有純函數的程式世界,設計了不同於一般程式語言的資料流程處理模式,這邊利用這個機會替函數(Function作個澄清)。
函數是函數式程式設計的主角,純函數就是數學上的函數,具備了引用透明性的函數就像是一根一根的水管,讓資料從水管的一頭流進,我們也能確定水管的另一頭流出什麼樣的資料。因此整個程式設計的工作就是將這些水管好好的接引,這就是所謂的函數式程式設計。只要我們能測試每一根水管都是沒有問題,那整個程式也就安全無虞。今日的分享就到這邊告一段落,明天再見。