F# 函式程式設計概念簡介

函式程式設計是一種強調使用函式和不可變資料的程式設計樣式。 當函式程式設計結合靜態類型 (例如 F#) 時,就會變成具類型函式程式設計。 一般而言,函式程式設計強調下列概念:

  • 函式是您使用的主要建構
  • 運算式而非陳述式
  • 不可變值而非變數
  • 宣告式程式設計而非命令式程式設計

在此系列中,您將探索使用 F# 進行函式程式設計的概念和模式。 在過程中,您也將了解一些 F#。

詞彙

函式程式設計與其他程式設計類型一樣,都有您終究必須了解的詞彙。 以下是您不時會看到的一些常見詞彙:

  • 函式 - 函式是一種建構,會在提供輸入時產生輸出。 更形式的說,它會將項目從一個集合「對應」至另一個集合。 這種形式可以透過許多方式具體化,特別是在使用與資料收集相關的作業函式時。 這是函式程式設計中最基本 (且重要) 的概念。
  • 運算式 - 運算式是程式碼中產生值的建構。 在 F# 中,此值必須加以繫結或明確忽略。 運算式可以輕鬆地取代為函式呼叫。
  • 單純性 - 單純性是函式的屬性,其傳回值針對相同的引數一律相同,而且其評估沒有副作用。 純函式完全相依於其引數。
  • 參考透明度 - 參考透明度是運算式的屬性,可以取代為其輸出,而不會影響程式的行為。
  • 不變性 - 不變性表示無法就地變更值。 這與可就地變更的變數相反。

範例

下列範例將示範這些核心概念。

函式

函式程式設計中最常見的基本建構是函式。 以下是將 1 加到某個整數的簡單函式:

let addOne x = x + 1

其類型簽章如下所示:

val addOne: x:int -> int

此簽章可以讀作「addOne 接受名為 xint,並會產生 int」。 更形式的說,addOne 會將值從一個整數集「對應」至另一個整數集。 此對應會以 -> 語彙基元表示。 在 F# 中,函式簽章通常一看就能了解其用途。

那麼,為什麼簽章很重要? 在具類型函式程式設計中,函式的實作與實際類型簽章相比,通常較不重要! addOne 將值 1 加到某個整數事實上在執行階段很有趣,但當您建構程式時,它會接受並傳回 int 的這點,其實才能讓您知道要如何實際使用此函式。 此外,一旦正確地使用此函式 (在其類型簽章方面),就只能在addOne 函式主體內進行任何問題診斷。 這是具類型程式設計背後的動力。

運算式

運算式是評估為值的建構。 相較於陳述式會執行動作,運算式會執行傳回值的動作。 在函式程式設計中,幾乎一律會使用運算式,而不是陳述式。

請考慮先前的函式 addOneaddOne 的主體是運算式:

// '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# 功能來說明這些概念。