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
将文本打印到控制台窗口。正如你可能猜到的那样,上面的代码打印出了 x
、y
和 z
的值。这个程序将产生以下结果
x: 5
y: 10
z: 15
注意 F# Interactive 用户:F# Interactive 中的所有语句都以
;;
(两个分号)结尾。要在 fsi 中运行上面的程序,将上面的文本复制并粘贴到 fsi 窗口中,输入;;
,然后按回车键。
在 F# 中,“变量”是一个误称。实际上,F# 中的所有“变量”都是不可变的;换句话说,一旦你将一个“变量”绑定到一个值,它将永远保持该值。因此,大多数 F# 程序员更喜欢使用“值”而不是“变量”来描述上面的 x
、y
和 z
。在幕后,F# 实际上将上面的“变量”编译为静态只读属性。
在 F# 中,函数和值之间几乎没有区别。你使用与声明值相同的语法来编写函数
let add x y = x + y
add
是函数的名称,它接受两个参数,x
和 y
。注意,函数声明中每个不同的参数都用空格隔开。同样,当你执行此函数时,连续的参数也用空格隔开
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
注意,我必须用括号将对 add
和 sub
函数的调用括起来;这告诉 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# 隐式地柯里化,因此在用两个参数调用函数时,有一个两步过程来解析函数
- 一个函数被第一个参数调用,该参数返回一个函数,该函数接受一个 float 并返回一个 float。为了帮助澄清柯里化,让我们将这个函数称为funX(注意,这个命名只是为了说明目的,运行时创建的函数是匿名的)。
- 第二个函数(上面第 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
的函数。泛型类型不同于具体类型,例如int
或string
;泛型类型表示稍后指定的类型。泛型函数很有用,因为它们可以推广到许多不同的类型。
让我们检查以下函数
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
,因此我们有一个类型不匹配。
后面的章节将演示如何在创造性和有趣的方式中使用泛型。