函数 (F#)
函数是任何编程语言中程序执行的基本单元。 与其他语言一样,F# 函数具有一个名称,可以具有形参并采用实参,还具有一个主体。 F# 还支持函数编程构造,例如,将函数视为值处理、在表达式中使用未命名的函数、将函数组合成新的函数、扩充函数,以及通过函数参数的部分应用来隐式定义函数。
可通过使用 let 关键字或 let rec 关键字组合(在函数为递归函数时)定义函数。
// Non-recursive function definition.
let [inline] function-name parameter-list [ : return-type ] = function-body
// Recursive function definition.
let rec function-name parameter-list = recursive-function-body
备注
function-name 是表示函数的标识符。 parameter-list 包含由空格分隔的连续参数。 可以按照“参数”节中的说明,为每个参数指定一个显式类型。 如果未指定特定参数类型,编译器将尝试从函数体推断出类型。 function-body 包含一个表达式。 组成函数体的表达式通常是一个由许多表达式组成的复合表达式,并且最后结束的最终表达式是返回值。 return-type 是一个冒号后跟一个类型,此为可选项。 如果不显式指定返回值的类型,则编译器将从最终的表达式确定返回类型。
一个简单的函数定义类似于下面这样:
let f x = x + 1
在前面的示例中,函数名称为 f,参数为 int 类型的 x,函数体为 x + 1,并且返回值的类型为 int。
内联说明符用于提示编译器,此函数是一个小型函数,其代码可以集成到调用方的主体中。
范围
在模块范围以外的任何范围级别上,重用某个值或函数名称不是错误。 在重用一个名称时,后来声明的名称将隐藏前面声明的名称。 但是,在模块中的顶级范围内,名称必须是唯一的。 例如,以下代码如果出现在模块范围内则会产生错误,而出现在某个函数内部则不会产生错误:
let list1 = [ 1; 2; 3]
// Error: duplicate definition.
let list1 = []
let function1 =
let list1 = [1; 2; 3]
let list1 = []
list1
但以下代码在任何级别的范围内都可接受:
let list1 = [ 1; 2; 3]
let sumPlus x =
// OK: inner list1 hides the outer list1.
let list1 = [1; 5; 10]
x + List.sum list1
参数
参数的名称列在函数名称之后。 可以为参数指定类型,如下面的示例所示:
let f (x : int) = x + 1
如果指定一个类型,则应让其跟在参数名称的后面,并用冒号与参数名称隔开。 如果省略参数的类型,则编译器会推断出参数类型。 例如,在下面的函数定义中,会将参数 x 推断为 int 类型,这是因为 1 属于 int 类型。
let f x = x + 1
但是,编译器会尝试使函数更具一般性。 例如,考虑下面的代码:
let f x = (x, x)
该函数从任何类型的一个参数中创建一个元组。 因为未指定类型,所以该函数可用于任何参数类型。 有关更多信息,请参见自动泛化 (F#)。
函数体
函数体可包含局部变量和函数的定义。 此类变量和函数在当前函数的主体范围内有效,但在此范围之外无效。 如果您已启用轻量语法选项,则必须使用缩进来指示定义是在函数体中,如以下示例所示:
let cylinderVolume radius length =
// Define a local value pi.
let pi = 3.14159
length * pi * radius * radius
有关更多信息,请参见代码格式设置准则 (F#)和详细语法 (F#)。
返回值
编译器使用函数体中的最终表达式来确定返回值和类型。 编译器可能会从前面的表达式来推断最终表达式的类型。 在前一节所示的函数 cylinderVolume 中,可从文本 3.14159 的类型确定 pi 的类型为 float。 编译器使用 pi 的类型来确定表达式 h * pi * r * r 的类型为 float。 因此,函数的总体返回类型为 float。
要显式指定返回值,应编写如下代码:
let cylinderVolume radius length : float =
// Define a local value pi.
let pi = 3.14159
length * pi * radius * radius
正如上面编写的代码一样,编译器将 float 应用于整个函数;如果您还打算将其应用于参数类型,可使用以下代码:
let cylinderVolume (radius : float) (length : float) : float
调用函数
可以通过以下方式调用函数:指定相应的函数名称,后跟一个空格,然后列出由空格分隔的任何参数。 例如,若要调用函数 cylinderVolume,并将结果赋给值 vol,可编写以下代码:
let vol = cylinderVolume 2.0 3.0
参数的部分应用
如果提供的参数数目少于指定的参数数目,则可创建一个应采用其余的参数的新函数。 这种处理参数的方法称为“扩充”,而且这是 F# 等函数编程语言的一个特性。 例如,假设您在处理两种尺寸的管道:一种管道的半径为 2.0,另一种管道的半径为 3.0。 您可能会创建用于确定管道体积的函数,如下所示:
let smallPipeRadius = 2.0
let bigPipeRadius = 3.0
// These define functions that take the length as a remaining
// argument:
let smallPipeVolume = cylinderVolume smallPipeRadius
let bigPipeVolume = cylinderVolume bigPipeRadius
然后,您会根据需要提供附加参数来表示两种尺寸的管道的不同长度:
let length1 = 30.0
let length2 = 40.0
let smallPipeVol1 = smallPipeVolume length1
let smallPipeVol2 = smallPipeVolume length2
let bigPipeVol1 = bigPipeVolume length1
let bigPipeVol2 = bigPipeVolume length2
递归函数
“递归函数”是调用自身的函数。 递归函数要求您在 let 关键字之后指定 rec 关键字。 从函数的主体内调用递归函数与调用任何函数调用是一样的。 下面的递归函数计算第 N 个斐波纳契数。 斐波那契数列很早就已经为人所知,此序列中的每个连续的数字都是前两个数字之和。
let rec fib n = if n < 2 then 1 else fib (n - 1) + fib (n - 2)
如果您在编写递归函数时不细心或者不清楚一些特殊技术(例如累加器和延续的使用),则有些递归函数可能会造成执行效率低下或使程序堆栈溢出。
函数值
在 F# 中,所有函数都被视为值;实际上,它们称作“函数值”。 因为函数是值,所以它们可用作其他函数的参数或在需要使用值的其他上下文中使用。 下面是一个采用函数值作为参数的函数的示例:
let apply1 (transform : int -> int ) y = transform y
可通过使用 -> 标记来指定函数值的类型。 此标记的左边是参数的类型,右边是返回值。 在前面的示例中,apply1 是一个采用函数 transform 作为参数的函数,其中的 transform 函数采用一个整数作为参数并返回另一个整数。 下面的代码演示如何使用 apply1:
let increment x = x + 1
let result1 = apply1 increment 100
运行前面的代码之后,result 的值将为 101。
多个参数之间由连续的 -> 标记分隔,如以下示例所示:
let apply2 ( f: int -> int -> int) x y = f x y
let mul x y = x * y
let result2 = apply2 mul 10 20
结果为 200。
Lambda 表达式
lambda 表达式是一个未命名的函数。 在前面的示例中,可以不定义命名函数 increment 和 mul,而使用如下的 lambda 表达式:
let result3 = apply1 (fun x -> x + 1) 100
let result4 = apply2 (fun x y -> x * y ) 10 20
可通过使用 fun 关键字来定义 lambda 表达式。 lambda 表达式类似于函数定义,只不过使用了 -> 标记来将参数列表与函数体分隔,而不是使用 = 标记。 与在常规函数定义中一样,可推断或显式指定参数类型,并且将从主体中最后一个表达式的类型推断出 lambda 表达式的返回类型。 有关更多信息,请参见Lambda 表达式:fun 关键字 (F#)。
函数组合和流水线处理
F# 中的函数可以由其他函数组合得到。 由 function1 和 function2 这两个函数组合而成的另一个函数表示先应用 function1,接着应用 function2:
let function1 x = x + 1
let function2 x = x * 2
let h = function1 >> function2
let result5 = h 100
结果为 202。
流水线处理方式可以让函数调用链接在一起形成连续的操作。 以下是流水线处理方式:
let result = 100 |> function1 |> function2
结果再次为 202。
组合运算符采用两个函数和函数返回;相反,管道运算符采用函数和参数和返回值。 下面的代码示例通过显示差异指出了管线和组合运算符之间的区别以及函数签名和用法。
// Function composition and pipeline operators compared.
let addOne x = x + 1
let timesTwo x = 2 * x
// Composition operator
// ( >> ) : ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3
let Compose2 = addOne >> timesTwo
// Backward composition operator
// ( << ) : ('T2 -> 'T3) -> ('T1 -> 'T2) -> 'T1 -> 'T3
let Compose1 = addOne << timesTwo
// Result is 5
let result1 = Compose1 2
// Result is 6
let result2 = Compose2 2
// Pipelining
// Pipeline operator
// ( <| ) : ('T -> 'U) -> 'T -> 'U
let Pipeline1 x = addOne <| timesTwo x
// Backward pipeline operator
// ( |> ) : 'T1 -> ('T1 -> 'U) -> 'U
let Pipeline2 x = addOne x |> timesTwo
// Result is 5
let result3 = Pipeline1 2
// Result is 6
let result4 = Pipeline2 2
重载函数
可以重载不是类型,但功能的方法。 有关更多信息,请参见方法 (F#)。