函数是任何编程语言中程序执行的基本单元。 与其他语言一样,F# 函数具有名称,可以具有参数并采用参数,并且具有正文。 F# 还支持函数编程构造,例如将函数视为值、在表达式中使用未命名函数、函数组合以形成新函数、curried 函数以及函数的隐式定义(通过部分应用函数参数)。
通过使用 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
注解
函数名称是表示函数的标识符。 参数列表由连续参数组成,这些参数由空格分隔。 可以为每个参数指定显式类型,如“参数”部分中所述。 如果未指定特定参数类型,编译器将尝试从函数正文推断类型。 函数主体由表达式组成。 构成函数主体的表达式通常是一个复合表达式,由许多表达式组成,这些表达式最终形成为返回值的最终表达式。 返回类型是后跟类型的冒号,是可选的。 如果未显式指定返回值的类型,编译器将确定最终表达式中的返回类型。
简单的函数定义如下所示:
let f x = x + 1
在上一示例中,函数名称为 f
,参数为 x
(具有类型 int
),函数体为 x + 1
,且返回值的类型为 int
。
可以标记 inline
函数。 有关信息 inline
,请参阅 内联函数。
Scope
在模块范围以外的任何范围级别,重复使用值或函数名称不是错误。 如果重复使用名称,则稍后声明的名称将隐藏前面声明的名称。 但是,在模块的顶级作用域中,名称必须是唯一的。 例如,以下代码在模块范围内显示时生成错误,但在函数内显示时不会生成错误:
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)
该函数从任意类型的一个参数创建元组。 由于未指定类型,因此该函数可与任何参数类型一起使用。 有关详细信息,请参阅 自动通用化。
函数主体
函数正文可以包含局部变量和函数的定义。 此类变量和函数位于当前函数的正文中,但不位于其外部。 必须使用缩进来指示定义在函数正文中,如以下示例所示:
let cylinderVolume radius length =
// Define a local value pi.
let pi = 3.14159
length * pi * radius * radius
返回值
编译器使用函数体中的最终表达式来确定返回值和类型。 编译器可能会从以前的表达式推断最终表达式的类型。 在函数cylinderVolume
中,如上一部分所示,其pi
类型由文本3.14159
float
的类型确定。 编译器使用类型pi
来确定表达式的类型length * pi * radius * radius
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
参数的部分应用程序
如果提供的参数数少于指定的参数数,则创建一个新函数,该函数需要剩余的参数。 这种处理参数的方法称为 currying ,是 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个 Fibonacci 数。 自古以来,Fibonacci 数字序列一直已知,并且是一个序列,其中每个连续数字是序列中前两个数字的总和。
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 表达式是未命名的函数。 在前面的示例中,可以使用 lambda 表达式,而不是定义命名函数 增量 和 mul,如下所示:
let result3 = apply1 (fun x -> x + 1) 100
let result4 = apply2 (fun x y -> x * y) 10 20
使用 fun
关键字定义 lambda 表达式。 lambda 表达式类似于函数定义,不同之处在于=
->
,令牌用于将参数列表与函数主体分开。 与正则函数定义中一样,参数类型可以显式推断或指定,并且 lambda 表达式的返回类型是从正文中最后一个表达式的类型推断出来的。 有关详细信息,请参阅 Lambda 表达式: fun
关键字。
Pipelines
在 F# 中处理数据时,广泛使用管道运算符 |>
。 此运算符允许你以灵活的方式建立函数的“管道”。 通过管道传送可将函数调用作为连续作链接在一起:
let result = 100 |> function1 |> function2
以下示例逐步讲解了如何使用这些运算符生成简单的功能管道:
/// Square the odd values of the input and add one, using F# pipe operators.
let squareAndAddOdd values =
values |> List.filter (fun x -> x % 2 <> 0) |> List.map (fun x -> x * x + 1)
let numbers = [ 1; 2; 3; 4; 5 ]
let result = squareAndAddOdd numbers
结果为 [2; 10; 26]
。 上一个示例使用列表处理函数,演示如何在生成管道时使用函数来处理数据。 管道运算符本身在 F# 核心库中定义,如下所示:
let (|>) x f = f x
函数组合
F# 中的函数可以由其他函数组成。 两个函数 function1 和 function2 的组合是另一个函数,表示 function1 的应用程序,后跟 function2 的应用程序:
let function1 x = x + 1
let function2 x = x * 2
let h = function1 >> function2
let result5 = h 100
结果是 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
// ( |> ) : 'T1 -> ('T1 -> 'U) -> 'U
let Pipeline2 x = addOne x |> timesTwo
// Backward pipeline operator
// ( <| ) : ('T -> 'U) -> 'T -> 'U
let Pipeline1 x = addOne <| timesTwo x
// Result is 5
let result3 = Pipeline1 2
// Result is 6
let result4 = Pipeline2 2
重载函数
可以重载类型的方法,但不能重载函数。 有关详细信息,请参阅 方法。