65.9K
CodeProject 正在变化。 阅读更多。
Home

我希望能在 C# 7 中写出的优雅代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.59/5 (11投票s)

2016年4月9日

CPOL

9分钟阅读

viewsIcon

35751

现在开始学习函数式编程范例!

引言

在我的上一篇文章《C# 7 即将迎来更好的函数式编程支持》中,我主张,当你有机会用 C# 7 编写代码时,你应该已经熟悉了函数式编程范式。但由于你目前还不能使用 C# 7,所以你绝对应该考虑用 F# 来学习函数式编程的概念。我希望我们能够像在 F# 中一样,在 C# 中编写优雅的声明式代码。

很难说服别人花时间去尝试新事物而看不到立竿见影的好处。但好处通常以惯用语言特性形式出现,这些特性起初可能令人望而生畏。我希望这篇文章能通过一个真实的F#代码示例,并逐一理解其特性,向您展示函数式编程的立竿见影的好处。我无意介绍F#的所有特性——只介绍足以让您对函数式编程感兴趣的特性。

最近我一直在看 Pluralsight 课程 F# Fundamentals by Liam McLennan,偶然发现了这段 精心设计的代码(快进到 0:46)

// F# Quicksort    
let rec quicksort = function
    | [] -> []
    | x :: xs ->
        let smaller = List.filter ((>) x) xs
        let larger = List.filter ((<=) x) xs
        quicksort smaller @ [x] @ quicksort larger    

这6行代码所包含的功能之多,令我惊叹不已!如果你是函数式编程特别是F#的新手,可能很难体会到这段代码的精妙之处,所以让我们逐个特性地回顾一下,是什么让这段代码如此优雅和强大。

如果您好奇 C# 中的快速排序实现是什么样子,请滚动到文章底部。

为了清晰和简单,我将只介绍与上述代码相关的特性,故意省略一些特性。例如,当我说“在 F# 中,值是不可变的”时,我省略了它们默认是不可变的,但你可以使用 **mutable** 修饰符或引用单元格等等。

 

函数式语言

一种语言要被认为是“函数式的”,通常需要支持几个关键特性:

  • 不可变数据
  • 函数组合能力
  • 函数可以作为数据处理
  • 模式匹配
  • 惰性求值

在本文中,我们将快速了解前四个特性。

 

Let 绑定

在F#中没有变量。您定义一个值并同时为其指定一个名称(标识符)。之后,您不能再为该名称赋新值。

要创建一个值,您可以使用 `let` 关键字通过 *let 绑定* 来实现。

// Creating a value via let binding
let x = 1

// F# Interactive output
val x : int = 1

上面那行代码创建了一个不可变的标识符 **x**,其值为 1。此外,F# 的类型推断系统推断出 **x** 的类型为 **int**。

F# 将函数视为数据,其值是函数体。要创建函数,同样使用 *let 绑定*

// Creating a function via let binding

// function declaration
let square x = x * x

// function signature in F# Interactive output
val square : x:int -> int

F# 中没有 **return** 关键字。函数的结果是最后计算的表达式。

注意函数签名中的箭头,它表示“这是一个函数,它接受一个类型为 **int** 的单值 **x** 并返回一个类型为 **int** 的值”。同样,是 F# 类型推断系统推断出函数的输入和输出值都将是 **int** 类型。

如果函数有多个参数,我们用空格将它们隔开

// Creating a function via let binding

// function declaration
let add x y = x + y

// function signature in F# Interactive output
val add : x:int -> y:int -> int

注意函数签名中有两个箭头,表示“这是一个函数,它接受两个值——**x** 和 **y**,它们都是 **int** 类型,并返回一个 **int** 类型的值”。

但更准确地说,它表示“这是一个函数,它接受一个 **int** 类型的 **x** 值,并返回一个函数,该函数接受一个 **int** 类型的 **y** 值,并返回一个 **int** 类型的值”。是不是有点糊涂了?继续阅读!

 

理解函数签名

当你查看函数的签名(而不是定义!)时,将每个箭头视为一个独立的函数,其输入在左侧,输出在右侧。

所以,当你看到 `typeA` **->** `typeB` 时,把它看作一个函数,它接受 `typeA` 类型的输入值并产生 `typeB` 类型的值。

同样,当你看到 `typeA` **->** `typeB` **->** `typeC` 时,你数一下箭头,然后从右到左在脑海中创建两个函数:`typeB` **->** `typeC` 是一个函数,它接受 `typeB` 类型的输入并产生 `typeC` 类型的值。你可以将这个函数命名为 f2。然后,`typeA` **->** `typeB` 是一个函数,它接受 `typeA` 类型的输入并产生 `typeB` 类型的值。你可以将这个函数命名为 f1。所以,你得到 `f1` -> `f2`。你可以将这两个函数连接起来,因为第一个函数的输出类型与第二个函数的输入类型相同。

 

函数应用

在 F# 中,您不说“我**调用**一个带参数 x 和 y 的函数 f”,而是说“我**应用**一个函数 f 到参数 x 和 y”。

所以,这里我们将函数 **square** 应用到类型为 **int** 的值 **3**。

// Function application
let squareResult = square 2

// F# Interactive output
val squareResult : int = 4

正如你所看到的,将 square 函数应用到其参数的结果是一个 int 类型的值。

这里我们将函数 **add** 应用到值 **3** 和 **5**,它们都是 **int** 类型。

// Function application
let addResult = add 3 5

// F# Interactive output
val addResult : int = 8

同样,将函数 add 应用于其参数的结果是一个 int 类型的值。

 

部分函数应用

当你应用函数但没有指定所有参数时会发生什么?换句话说——你将函数应用于部分参数集?在命令式语言中你会得到一个错误,但在 F# 中你会得到一个部分应用的函数。

让我们看看当我们只将函数 add 应用于它的第一个参数时会得到什么。

// Partial function application
let addThreeTo = add 3

// addThreeTo signature in F# Interactive output
val addThreeTo : (int -> int)

你注意到箭头了吗?它是一个函数!它的签名略有不同——周围有括号,但它仍然是一个函数。

因此,**addThreeTo** 是一个部分应用的函数 **add**,其中值 **3** “嵌入”到它的第一个参数中。从签名我们可以看到,它是一个 **函数**,它接受一个 **int** 类型的参数并返回一个 **int** 类型的值。由于它是一个函数——我们可以将其应用于其他参数!

// Applying partially applied function to its arguments
addThreeTo 1

// F# Interactive output
val it : int = 4

您可以清楚地看到,将部分应用的函数应用于其参数的结果只是一个普通的 **int** 类型值。

 

匿名函数 / Lambda 表达式

之前,当我们定义函数体时,我们将其绑定到一个标识符,以便稍后可以使用它。但如果我们要将代码捆绑在一起仅用于局部应用,并且不打算稍后重用或不想污染命名空间,我们可以使用匿名函数,也称为 lambda 表达式,来内联创建函数。

要创建 lambda 表达式,您使用 **fun** 关键字,后跟函数参数和一个箭头。因此,我们可以使用以下两个 lambda 表达式代替我们的 **square** 和 **add** 函数

// Lambda expression
(fun x -> x * x) 2

// F# Interactive output
val it : int = 4

// Lambda expression
(fun x y -> x + y) 3 5

// F# Interactive output
val it : int = 8

 

高阶函数

因为在F#中,函数只是一个值,所以其他函数,称为高阶函数,可以接受函数作为参数并将其视为返回值。

假设我们想有一个函数,它将不同的算法应用于两个值。我们不知道那个算法会是什么,我们只知道它接受两个值。因此,参数 **f** 将是一个函数(一个算法),我们将它应用于参数 **x** 和 **y**。让我们使用 lambda 表达式来指定不同的算法。

// Higher-order functions

let applyAlgorithm f x y = f x y

applyAlgorithm (fun x y -> x + y) 3 5

// F# Interactive output
val it : int = 8

applyAlgorithm (fun x y -> x * y) 3 5

// F# Interactive output
val it : int = 15

来点乐趣怎么样!如果我们要部分地将算法应用于它的第一个参数呢?令人费解,但在 F# 中却轻而易举!

let multiplyByThree = applyAlgorithm (fun x y -> x * y) 3

multiplyByThree 5
// F# Interactive output
val it : int = 15

multiplyByThree 6
// F# Interactive output
val it : int = 18

multiplyByThree 7
// F# Interactive output
val it : int = 21    

 

F# 语法糖

如果 lambda 表达式只有一个参数,并且它被用作函数体中的最后一个参数,您可以省略 **fun** 关键字、**箭头**、**参数** 和 **参数本身**。

// F# syntactic sugar

let formatOutput f x = f x

formatOutput (fun x -> sprintf "The value is %d" x) 5

formatOutput (sprintf "The value is %d") 5

// F# Interactive output
val it : string = "The value is 5"

 

符号运算符

当你应用一个函数时,你使用所谓的 **后缀** 表示法,这意味着你应用函数的参数跟在函数名称之后。

// Postfix notation
add 1 2

但是使用后缀表示法表达一些数学公式会相当难以阅读

// Postfix notation
(mult (add 1 2) (add 3 4))

为了这些目的,您可以创建一个符号运算符——一个函数,其名称由以下符号的任意序列组成:**!%&*+-./@^|?**

在函数定义中,您需要将这些符号放在括号中。

// Symbolic operator

let (@%@) x y = x + y

// F# Interactive output
val ( @%@ ) : x:int -> y:int -> int

// yep, it's nothing more than just a function

默认情况下,当您将符号运算符应用于其参数时,您使用 **中缀** 表示法,这意味着您将第一个参数放在符号运算符之前,第二个参数放在符号运算符之后。

// Symbolic operator - infix notation
3 @%@ 4

// F# Interactive output
val it : int = 7

如果你想在应用符号运算符时使用后缀表示法——把它放在括号里。

// Symbolic operator - postfix notation
(@%@) 3 4

// F# Interactive output
val it : int = 7

当您想要将符号运算符部分应用于参数时,您可以使用后缀表示法。

// Partially applied symbolic operator
let partiallyAppliedSymbolicOperator = (@%@) 3

// F# Interactive output
val partiallyAppliedSymbolicOperator : (int -> int)

partiallyAppliedSymbolicOperator 4

// F# Interactive output
val it : int = 7

 

递归函数

要定义一个可以调用自身的函数,请使用 **rec** 关键字。

// Recursive function
let rec factorial n =
    if n < 0 then
        failwith "The value n cannot be negative"
    elif n <= 1 then
        1
    else
        n * factorial (n - 1)

factorial 5

// F# Interactive output
val it : int = 120

 

F# 列表

在 F# 中,列表是以分号分隔的不可变值集合,用方括号括起来。值必须是相同类型。

// F# list
let numbers = [1; 2; 3; 4]

// F# Interactive output
val numbers : int list = [1; 2; 3; 4]

要访问列表元素,请使用基于零的点表示法。

// F# list
let secondNumber = numbers.[1]

// F# Interactive output
val secondNumber : int = 2

空列表用空方括号表示。

// F# list
[]

// F# Interactive output
val it : 'a list = []

您只能对列表执行两种操作。第一种是 **cons**(由 cons 运算符 **::** 表示),它将一个元素连接到列表的开头或头部,生成一个新列表。第二种是 **append**(由 **@** 运算符表示),它将两个列表连接在一起,生成一个新列表。

// F# list cons operation
let smallerThanFive = 0 :: numbers

// F# Interactive output
val smallerThanFive : int list = [0; 1; 2; 3; 4]
// F# list append operation
let largerThanFive = [6; 7; 8; 9]

let allNumbers = smallerThanFive @ [5] @ largerThanFive

// F# Interactive output
val allNumbers : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]

 

模式匹配

模式匹配是函数式语言中强大而复杂的特性之一。你可以把它看作一个强大的 switch 语句,它可以匹配输入值的类型,提取值,执行一些操作等等。

在以下代码中,**match** 表达式试图将 **number** 参数与常量模式 **1**、**2** 或变量模式 **n** 进行匹配。

// Pattern matching
let describeNumber number =
    match number with
    | 1 -> printfn "The number is 1"
    | 2 -> printfn "The number is 2"
    | n -> printfn "The number is %d" n


describeNumber 1
// The number is 1

describeNumber 2
// The number is 2

describeNumber 5
// The number is 5

在以下代码中,**match** 表达式试图将 **list** 参数与列表模式 **[]**、**[_]**、**[_;_]** 或通配符模式 **_** 进行匹配,以获取列表中元素的数量。

// Pattern matching
let describeList list =
    match list with
    | [] -> printfn "Empty list"
    | [_] -> printfn "The list with one element"
    | [_;_] -> printfn "The list with two elements"
    | _ -> printfn "The list with %d elements" (List.length list)


describeList []
// Empty list

describeList [1]
// The list with one element

describeList [1; 2]
// The list with two elements

describeList [1; 2; 3; 4; 5; 6]
// The list with 6 elements

在以下代码中,**match** 表达式试图将 **list** 参数与列表模式 **[]** 和 cons 模式 **h :: t** 进行匹配,将列表分成头部元素和尾部列表。

// Pattern matching
let splitList list =
    match list with
    | [] -> printfn "Empty list"
    | head :: tail -> printfn "head: %A\ntail: %A" head tail


splitList []
// Empty list

splitList [1]
// head: 1
// tail: []

splitList [1; 2]
// head: 1
// tail: [2]

splitList [1; 2; 3; 4; 5; 6]
// head: 1
// tail: [2; 3; 4; 5; 6]

您还可以使用模式匹配来代替 **if** 语句。我们可以使用带有 **when guard** 的模式匹配重写阶乘算法。

// Pattern matching in recursive function
let rec factorial n =
    match n with
    | n when n < 0  -> failwith "The value n cannot be negative"
    | n when n <= 1 -> 1
    | n             -> n * factorial (n - 1)

factorial 5

// F# Interactive output
val it : int = 120

 

带有模式匹配的 `function` 关键字

您可以使用简化的 lambda 语法与模式匹配。请注意,在这种情况下,您没有显式指定函数参数,因为 **function** lambda 只接受一个直接进入模式匹配表达式的参数。

// Pattern matching in recursive function
let rec factorial = function
    | n when n < 0  -> failwith "The value n cannot be negative"
    | n when n <= 1 -> 1
    | n             -> n * factorial (n - 1)

factorial 5

// F# Interactive output
val it : int = 120

 

List 模块函数

在 F# 中,**模块** 是 F# 代码的分组。List 模块包含您可以应用于 F# 列表的函数。让我们来看其中的几个。

List.length – 给定一个列表,返回其长度

List.length;;
// ('a list -> int)

List.length [1; 2; 3];;
// val it : int = 3    

List.iter – 给定一个函数和一个列表,遍历列表并将函数应用于每个元素。

List.iter;;
// (('a -> unit) -> 'a list -> unit)

List.iter (fun n -> printfn "Value: %d" n) [1; 2; 3];;
// Value: 1
// Value: 2
// Value: 3

// or you could re-write it as
List.iter (printfn "Value: %d") [1; 2; 3];;    

List.filter – 给定一个函数和一个列表,遍历列表并将该函数应用于每个元素,如果函数返回 false,则从结果列表中消除这些元素。

List.filter;;
// (('a -> bool) -> 'a list -> 'a list)

List.filter (fun n -> 3 > n) [1; 2; 3; 4; 5];;
// val it : int list = [1; 2]

// or you could re-write it
// using symbolic operator postfix notation

List.filter ((>) 3) [1; 2; 3; 4; 5]
// val it : int list = [1; 2]    

 

将所有内容整合

此时,您应该已经掌握了理解和欣赏文章开头代码所需的一切。为了方便起见,我将再次在这里重复一遍。

// F# Quicksort
let rec quicksort = function
    | [] -> []
    | x :: xs ->
        let smaller = List.filter ((>) x) xs
        let larger = List.filter ((<=) x) xs
        quicksort smaller @ [x] @ quicksort larger

let sorted = quicksort [4;5;4;7;9;1;6;1;0;-99;10000;3;2]

// F# Interactive output
// val sorted : int list = [-99; 0; 1; 1; 2; 3; 4; 4; 5; 6; 7; 9; 10000]

通过查看这种声明式代码,很容易看出快速排序是一个应用于列表的递归函数。该函数使用模式匹配将列表分成头部和尾部,然后创建两个未排序的列表,其中包含比头部小和大的数字,将函数本身应用于这些列表,最后一步——将这些现在已排序的列表与头部拼接起来,生成最终的已排序列表。呼……

 

C# 中快速排序算法的命令式实现

来自 Pluralsight 课程 F# Fundamentals by Liam McLennan 的 C# 快速排序(快进到 3:02)

// C# Quicksort
public static void Quicksort(int[] elements, int left, int right) {
	int i = left, j = right; int pivot = elements[(left + right) / 2];
	while (i <= j) {
		while (elements[i] < pivot) { i++; }
		while (elements[j] > pivot) { j--; }
		if (i <= j) {
			int tmp = elements[i];
			elements[i] = elements[j];
			elements[j] = tmp;
			i++; j--;
		}
	}
	if (left < j) { Quicksort(elements, left, j); }
	if (i < right) { Quicksort(elements, i, right); }
}

 

参考文献

1. Liam McLennan 的 F# 基础
2. Chris Smith 的《F# 3.0 编程(第二版)》

© . All rights reserved.