跳转到内容

F# 编程/值与函数

来自维基教科书,开放的书籍,为开放的世界
上一个:基本概念 索引 下一个:模式匹配基础
F#:声明值与函数

与其他 .NET 语言(如 C# 和 VB.Net)相比,F# 具有比较简洁和极简的语法。要跟随本教程,请打开 F# Interactive (fsi) 或 Visual Studio 并运行示例。

声明变量

[编辑 | 编辑源代码]

F# 中最普遍、最熟悉的关键字是 let 关键字,它允许程序员在应用程序中声明函数和变量。

例如

let x = 5

这声明了一个名为 x 的变量,并将其赋值为 5。自然,我们可以写出以下内容

let x = 5
let y = 10
let z = x + y

z 现在持有值 15。

完整的程序如下所示

let x = 5
let y = 10
let z = x + y

printfn "x: %i" x
printfn "y: %i" y
printfn "z: %i" z

语句 printfn 将文本打印到控制台窗口。正如你可能猜到的那样,上面的代码打印出了 xyz 的值。这个程序将产生以下结果

x: 5
y: 10
z: 15

注意 F# Interactive 用户:F# Interactive 中的所有语句都以 ;;(两个分号)结尾。要在 fsi 中运行上面的程序,将上面的文本复制并粘贴到 fsi 窗口中,输入 ;;,然后按回车键。

值,而不是变量

[编辑 | 编辑源代码]

在 F# 中,“变量”是一个误称。实际上,F# 中的所有“变量”都是不可变的;换句话说,一旦你将一个“变量”绑定到一个值,它将永远保持该值。因此,大多数 F# 程序员更喜欢使用“值”而不是“变量”来描述上面的 xyz。在幕后,F# 实际上将上面的“变量”编译为静态只读属性。

声明函数

[编辑 | 编辑源代码]

在 F# 中,函数和值之间几乎没有区别。你使用与声明值相同的语法来编写函数

let add x y = x + y

add 是函数的名称,它接受两个参数,xy。注意,函数声明中每个不同的参数都用空格隔开。同样,当你执行此函数时,连续的参数也用空格隔开

let z = add 5 10

这将 z 赋值为该函数的返回值,在本例中恰好是 15

自然,我们可以将函数的返回值直接传递到其他函数中,例如

let add x y = x + y

let sub x y = x - y

let printThreeNumbers num1 num2 num3 =
    printfn "num1: %i" num1
    printfn "num2: %i" num2
    printfn "num3: %i" num3

printThreeNumbers 5 (add 10 7) (sub 20 8)

这个程序输出

num1: 5
num2: 17
num3: 12

注意,我必须用括号将对 addsub 函数的调用括起来;这告诉 F# 将括号中的值视为单个参数。

否则,如果我们写 printThreeNumbers 5 add 10 7 sub 20 8,不仅难以阅读,而且实际上向函数传递了 7 个参数,这显然是错误的。

函数返回值

[编辑 | 编辑源代码]

与许多其他语言不同,F# 函数没有明确的关键字来返回值。相反,函数的返回值只是函数中执行的最后一条语句的值。例如

let sign num =
    if num > 0 then "positive"
    elif num < 0 then "negative"
    else "zero"

该函数接受一个整数参数并返回一个字符串。正如你可以想象的那样,上面的 F# 函数等同于以下 C# 代码

string Sign(int num)
{
    if (num > 0) return "positive";
    else if (num < 0) return "negative";
    else return "zero";
}

就像 C# 一样,F# 是一种强类型语言。一个函数只能返回一种数据类型;例如,以下 F# 代码将无法编译

let sign num =
    if num > 0 then "positive"
    elif num < 0 then "negative"
    else 0

如果你在 fsi 中运行这段代码,你会收到以下错误消息

> let sign num =
    if num > 0 then "positive"
    elif num < 0 then "negative"
    else 0;;

      else 0;;
  ---------^
stdin(7,10): error FS0001: This expression was expected to have type    string    but here has type    int   

错误消息非常明确:F# 确定此函数返回一个 string,但函数的最后一行返回了一个 int,这是一个错误。

有趣的是,F# 中的每个函数都有一个返回值;当然,程序员并不总是编写返回有意义值的函数。F# 有一个特殊的数据类型称为 unit,它只有一个可能的值:()。当函数不需要向程序员返回值时,它们会返回 unit。例如,将字符串打印到控制台的函数显然没有返回值

let helloWorld () = printfn "hello world"

该函数接受 unit 参数并返回 ()。你可以将 unit 视为 C 样式语言中 void 的等价物。

如何阅读箭头符号

[编辑 | 编辑源代码]

F# 中的所有函数和值都有一个数据类型。打开 F# Interactive 并输入以下内容

> let addAndMakeString x y = (x + y).ToString();;

F# 使用链式箭头符号报告数据类型,如下所示

val addAndMakeString : x:int -> y:int -> string

数据类型是从左到右读取的。在用更准确的描述弄乱 F# 函数的构建方式之前,请考虑箭头符号的基本概念:从左侧开始,我们的函数接受两个 int 输入并返回一个 string。一个函数只有一个返回类型,由链式箭头符号中最右边的数据类型表示。

我们可以将以下数据类型读作

int -> string

接受一个 int 输入,返回一个 string

float -> float -> float

接受两个 float 输入,返回另一个 float

int -> string -> float

接受一个 int 和一个 string 输入,返回一个 float

这个描述对于初学者来说是一个很好的入门方式来理解箭头符号,如果你刚接触 F#,可以在这里停下来,直到你上手为止。对于那些觉得这个概念很舒服的人,F# 实际实现这些调用的方式是通过柯里化函数。

部分函数应用

[编辑 | 编辑源代码]

虽然上面的箭头符号描述直观,但它并不完全准确,因为F# 隐式地柯里化 函数。这意味着一个函数永远只有一个参数和一个返回类型,这与上面箭头符号的描述大相径庭,在第二和第三个示例中,两个参数被传递给一个函数。实际上,F# 中的函数永远只有一个参数和一个返回类型。这怎么可能呢?考虑这个类型

float -> float -> float

由于这种类型的函数被 F# 隐式地柯里化,因此在用两个参数调用函数时,有一个两步过程来解析函数

  1. 一个函数被第一个参数调用,该参数返回一个函数,该函数接受一个 float 并返回一个 float。为了帮助澄清柯里化,让我们将这个函数称为funX(注意,这个命名只是为了说明目的,运行时创建的函数是匿名的)。
  2. 第二个函数(上面第 1 步中的'funX')用第二个参数调用,返回一个 float

因此,如果你提供两个 float,结果看起来就像函数接受两个参数一样,但实际上运行时的行为并非如此。柯里化的概念可能会让没有深入研究函数概念的开发人员感到非常奇怪和不直观,甚至不必要地冗余和低效,因此,在尝试进一步解释之前,请考虑柯里化函数的优点,通过一个例子

let addTwoNumbers x y = x + y

这个类型有以下签名

int -> int -> int

那么这个函数

let add5ToNumber = addTwoNumbers 5

的类型签名为 (int -> int)。注意 add5ToNumber 的主体只用一个参数调用 addTwoNumbers,而不是两个。它返回一个函数,该函数接受一个 int 并返回一个 int。换句话说,add5toNumber部分应用addTwoNumbers 函数。

> let z = add5ToNumber 6;;

val z : int = 11

这个用多个参数对函数进行部分应用体现了柯里化函数的强大功能。它允许延迟应用函数,从而实现更模块化的开发和代码重用,我们可以通过部分应用来重用 addTwoNumbers 函数来创建一个新函数。由此,你可以体会到函数柯里化的强大功能:它总是将函数应用分解为最小的元素,为代码重用和模块化提供更多机会。

再举一个例子,说明如何将部分应用的函数用作记账技术。注意 holdOn 的类型签名是一个函数 (int -> int),因为它是对 addTwoNumbers 的部分应用

> let holdOn = addTwoNumbers 7;;

val holdOn : (int -> int)

> let okDone = holdOn 8;;

val okDone : int = 15

这里我们动态定义一个新的函数holdOn,仅仅为了跟踪要添加的第一个值。然后,我们使用另一个值应用这个新的“临时”函数holdOn,它返回一个int。由柯里化实现的部分应用函数是控制 F# 中复杂性的一个非常强大的手段。简而言之,柯里化函数调用导致间接的原因是它允许部分函数应用,以及由此带来的所有好处。换句话说,部分函数应用的目标是通过隐式柯里化实现的。

因此,虽然箭头符号是理解函数类型签名的良好简写,但它以过度简化的代价实现了这一点,因为具有以下类型签名的函数

f : int -> int -> int

实际上(当考虑隐式柯里化时)

// curried version pseudo-code
f: int -> (int -> int)

换句话说,f 是一个函数,它接受一个 int 并返回一个函数,该函数接受一个 int 并返回一个 int。此外,

f: int -> int -> int -> int

// curried version pseudo-code
f: int -> (int -> (int -> int))

的简写,或者,用非常难以解码的英语来说:f 是一个函数,它接受一个 int 并返回一个函数,该函数接受一个 int 并返回一个函数,该函数接受一个 int 并返回一个 int。哎呀!

嵌套函数

[edit | edit source]

F# 允许程序员在其他函数内部嵌套函数。嵌套函数有很多应用,例如隐藏内部循环的复杂性

let sumOfDivisors n =
    let rec loop current max acc =
        if current > max then
            acc
        else
            if n % current = 0 then
                loop (current + 1) max (acc + current)
            else
                loop (current + 1) max acc
    let start = 2
    let max = n / 2     (* largest factor, apart from n, cannot be > n / 2 *)
    let minSum = 1 + n  (* 1 and n are already factors of n *)
    loop start max minSum

printfn "%d" (sumOfDivisors 10)
(* prints 18, because the sum of 10's divisors is 1 + 2 + 5 + 10 = 18 *)

外部函数sumOfDivisors调用内部函数loop。程序员可以根据需要设置任意级别的嵌套函数。

泛型函数

[edit | edit source]

在编程中,泛型函数是指不牺牲类型安全性的情况下返回不确定类型t的函数。泛型类型不同于具体类型,例如intstring;泛型类型表示稍后指定的类型。泛型函数很有用,因为它们可以推广到许多不同的类型。

让我们检查以下函数

let giveMeAThree x = 3

F# 从变量在应用程序中的使用方式推导出变量的类型信息,但 F# 无法将值x限制为任何特定的具体类型,因此 F# 将x推广到泛型类型'a

'a -> int

此函数接受一个泛型类型'a并返回一个int

当您调用泛型函数时,编译器会将函数的泛型类型替换为传递给函数的值的数据类型。作为演示,让我们使用以下函数

let throwAwayFirstInput x y = y

它的类型为'a -> 'b -> 'b,这意味着该函数接受一个泛型'a和一个泛型'b并返回一个'b

以下是一些 F# 交互式环境中的示例输入和输出

> let throwAwayFirstInput x y = y;;

val throwAwayFirstInput : 'a -> 'b -> 'b

> throwAwayFirstInput 5 "value";;
val it : string = "value"

> throwAwayFirstInput "thrownAway" 10.0;;
val it : float = 10.0

> throwAwayFirstInput 5 30;;
val it : int = 30

throwAwayFirstInput 5 "value" 用一个int和一个string调用该函数,它将int替换为'a,将string替换为'b。这将throwAwayFirstInput的数据类型更改为int -> string -> string

throwAwayFirstInput "thrownAway" 10.0 用一个string和一个float调用该函数,因此函数的数据类型更改为string -> float -> float

throwAwayFirstInput 5 30 恰好用两个int调用该函数,因此函数的数据类型碰巧是int -> int -> int

泛型函数是强类型的。例如

let throwAwayFirstInput x y = y
let add x y = x + y

let z = add 10 (throwAwayFirstInput "this is a string" 5)

泛型函数throwAwayFirstInput被重新定义,然后add函数被定义,它的类型是int -> int -> int,这意味着此函数必须使用两个int参数调用。

然后,throwAwayFirstInput被调用,作为add的参数,它自身带有两个参数,第一个是字符串类型,第二个是整数类型。对throwAwayFirstInput的这次调用最终具有类型string -> int -> int。由于该函数的返回类型是int,因此代码按预期工作

> add 10 (throwAwayFirstInput "this is a string" 5);;
val it : int = 15

但是,当我们颠倒throwAwayFirstInput参数的顺序时,会出现错误

> add 10 (throwAwayFirstInput 5 "this is a string");;

  add 10 (throwAwayFirstInput 5 "this is a string");;
  ------------------------------^^^^^^^^^^^^^^^^^^^

stdin(13,31): error FS0001: This expression has type
        string
but is here used with type
        int.

错误消息非常明确:add函数需要两个int参数,但throwAwayFirstInput 5 "this is a string"的返回类型是string,因此我们有一个类型不匹配。

后面的章节将演示如何在创造性和有趣的方式中使用泛型。

上一个:基本概念 索引 下一个:模式匹配基础
华夏公益教科书