F# 函式程式設計概念簡介
函式程式設計是一種強調使用函式和不可變資料的程式設計樣式。 當函式程式設計結合靜態類型 (例如 F#) 時,就會變成具類型函式程式設計。 一般而言,函式程式設計強調下列概念:
- 函式是您使用的主要建構
- 運算式而非陳述式
- 不可變值而非變數
- 宣告式程式設計而非命令式程式設計
在此系列中,您將探索使用 F# 進行函式程式設計的概念和模式。 在過程中,您也將了解一些 F#。
詞彙
函式程式設計與其他程式設計類型一樣,都有您終究必須了解的詞彙。 以下是您不時會看到的一些常見詞彙:
- 函式 - 函式是一種建構,會在提供輸入時產生輸出。 更形式的說,它會將項目從一個集合「對應」至另一個集合。 這種形式可以透過許多方式具體化,特別是在使用與資料收集相關的作業函式時。 這是函式程式設計中最基本 (且重要) 的概念。
- 運算式 - 運算式是程式碼中產生值的建構。 在 F# 中,此值必須加以繫結或明確忽略。 運算式可以輕鬆地取代為函式呼叫。
- 單純性 - 單純性是函式的屬性,其傳回值針對相同的引數一律相同,而且其評估沒有副作用。 純函式完全相依於其引數。
- 參考透明度 - 參考透明度是運算式的屬性,可以取代為其輸出,而不會影響程式的行為。
- 不變性 - 不變性表示無法就地變更值。 這與可就地變更的變數相反。
範例
下列範例將示範這些核心概念。
函式
函式程式設計中最常見的基本建構是函式。 以下是將 1 加到某個整數的簡單函式:
let addOne x = x + 1
其類型簽章如下所示:
val addOne: x:int -> int
此簽章可以讀作「addOne
接受名為 x
的 int
,並會產生 int
」。 更形式的說,addOne
會將值從一個整數集「對應」至另一個整數集。 此對應會以 ->
語彙基元表示。 在 F# 中,函式簽章通常一看就能了解其用途。
那麼,為什麼簽章很重要? 在具類型函式程式設計中,函式的實作與實際類型簽章相比,通常較不重要! addOne
將值 1 加到某個整數事實上在執行階段很有趣,但當您建構程式時,它會接受並傳回 int
的這點,其實才能讓您知道要如何實際使用此函式。 此外,一旦正確地使用此函式 (在其類型簽章方面),就只能在addOne
函式主體內進行任何問題診斷。 這是具類型程式設計背後的動力。
運算式
運算式是評估為值的建構。 相較於陳述式會執行動作,運算式會執行傳回值的動作。 在函式程式設計中,幾乎一律會使用運算式,而不是陳述式。
請考慮先前的函式 addOne
。 addOne
的主體是運算式:
// 'x + 1' is an expression!
let addOne x = x + 1
此運算式的結果會定義 addOne
函式的結果類型。 例如,組成此函式的運算式可以變更為不同的類型 (如 string
):
let addOne x = x.ToString() + "1"
函式的簽章現在是:
val addOne: x:'a -> string
由於可對 F# 中的任何類型呼叫 ToString()
,因此 x
的類型已變成泛型 (稱為自動一般化),而結果類型為 string
。
運算式不只是函式的主體。 您也可以讓運算式產生要在其他地方使用的值。 一個常見的範例是 if
:
// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0
let addOneIfOdd input =
let result =
if isOdd input then
input + 1
else
input
result
if
運算式會產生稱為 result
的值。 請注意,您可以完全省略 result
,讓 if
運算式成為 addOneIfOdd
函式的主體。 請記住,運算式的關鍵重點是其會產生值。
當沒有任何內容可傳回時,會使用一種特殊類型 unit
。 例如,請考慮下列簡單函式:
let printString (str: string) =
printfn $"String is: {str}"
簽章看起來如下:
val printString: str:string -> unit
unit
類型表示不會傳回實際值。 當您有一個必須「執行工作」的常式,但沒有值可傳回作為該工作的結果時,這會很有用。
這與命令式程式設計形成強烈的對比,在命令式程式設計中,對等 if
建構是陳述式,而且通常會透過變動變數來產生值。 例如,在 C# 中,程式碼可能會撰寫如下:
bool IsOdd(int x) => x % 2 != 0;
int AddOneIfOdd(int input)
{
var result = input;
if (IsOdd(input))
{
result = input + 1;
}
return result;
}
值得注意的是,C# 和其他 C 樣式語言確實支援三元運算式,因此可進行以運算式為基礎的條件式程式設計。
在函式程式設計中,很少會使用陳述式來變動值。 雖然某些函式語言支援陳述式和變動,但在函式程式設計中使用這些概念並不常見。
純函式
如前所述,純函式是符合下列條件的函式:
- 針對相同的輸入一律會評估為相同的值。
- 沒有副作用。
在此內容中思考數學函式會有所幫助。 在數學中,函式只相依於其引數,而且沒有任何副作用。 在數學函式 f(x) = x + 1
中,f(x)
的值只相依於 x
的值。 函式程式設計中的純函式也是如此。
撰寫純函式時,函式必須只相依於其引數,而且不會執行任何導致副作用的動作。
以下是非純函式的範例,因為它相依於全域可變狀態:
let mutable value = 1
let addOneToValue x = x + value
addOneToValue
函式顯然不純,因為 value
可能會隨時變更為具有與 1 不同的值。 在函式程式設計中,應避免使用相依於全域值的這個模式。
以下是非純函式的另一個範例,因為它會執行副作用:
let addOneToValue x =
printfn $"x is %d{x}"
x + 1
雖然此函式不相依於全域值,但會將 x
的值寫入程式的輸出。 雖然這樣做本身沒有錯,但確實表示函式為非純函式。 如果程式的另一個部分相依於程式外部的某個項目 (例如輸出緩衝區),則呼叫此函式可能會影響程式的其他部分。
移除 printfn
陳述式會讓函式變成純函式:
let addOneToValue x = x + 1
雖然此函式本身不見得比使用 printfn
陳述式的舊版「更好」,但確實保證此函式只會傳回值。 無論呼叫此函式多少次,都會產生相同的結果:它只會產生值。 單純性提供了許多函式程式設計人員力求的可預測性。
不變性
最後,具類型函式程式設計的最基本概念之一是不變性。 在 F# 中,所有值預設都是不可變。 這表示除非您明確地將其標記為可變動,否則無法就地變動。
實際上,使用不可變值表示您將程式設計的方法從「我需要變更某個項目」變更為「我需要產生新的值」。
例如,將 1 加到某個值會產生新的值,而不是變動現有的值:
let value = 1
let secondValue = value + 1
在 F# 中,下列程式碼「不會」變動 value
函式,而是會執行相等檢查:
let value = 1
value = value + 1 // Produces a 'bool' value!
某些函式程式設計語言完全不支援變動。 在 F# 中,它會受到支援,但不是值的預設行為。
此概念可更進一步延伸到資料結構。 在函式程式設計中,不可變資料結構 (例如集合等) 會有不同於您一開始可能預期的實作。 就概念而言,將項目新增至集合之類的動作不會變更集合,而是會產生具有新增值的「新」集合。 實際上,這通常是由不同的資料結構來完成,以便有效率地追蹤值,而能提供適當的資料表示作為結果。
這種使用值和資料結構的樣式很重要,因為它會強制您將任何修改某個項目的作業,當做像是建立該項目的新版本一樣。 這可讓等號比較之類的項目在您的程式中保持一致。
下一步
下一節將完整說明函式,並探索您可以在函式程式設計中使用函式的不同方式。
在 F# 中使用函式會深入探索函式,並說明如何在各種內容中使用函式。
進一步閱讀
以函式思考 (英文) 系列是另一個絕佳的資源,有助於了解 F# 函式程式設計。 文中以實用且容易閱讀的方式來說明函式程式設計的基本概念,並使用 F# 功能來說明這些概念。