函数编程是一种编程样式,强调函数的使用和不可变数据。 类型化函数编程是在函数编程与静态类型(如 F#)结合使用时。 一般情况下,函数编程中强调以下概念:
- 函数作为你使用的主构造
- 表达式而不是语句
- 变量的不可变值
- 声明性编程优于命令式编程
在本系列中,你将使用 F# 探索函数编程的概念和模式。 在此过程中,你也将了解一些 F#。
术语
函数编程与其他编程范例一样,附带了一个词汇,你最终需要学习。 下面是一些常用术语,你会经常看到:
- 函数 - 函数 是一个构造,将在给定输入时生成输出。 更正式地说,它将项从一组 映射到 另一组。 这种形式主义在许多方面被具体化,尤其是在使用对数据集操作的函数时。 它是函数编程中最基本的(和重要的)概念。
- 表达式 - 表达式是代码中生成值的构造。 在 F# 中,此值必须绑定或显式忽略。 可以通过函数调用来简单替换表达式。
- 纯洁 - 纯洁 是函数的一个属性,因此其返回值对于同一参数始终相同,并且其计算没有副作用。 纯函数完全取决于其参数。
- 引用透明度 - 引用透明度是表达式的属性,因此可以将其替换为其输出,而不会影响程序的行为。
- 不可变性 - 不可变性意味着无法就地更改值。 这与变量不同,后者可以就地更改。
例子
以下示例演示了这些核心概念。
功能
函数编程中最常见的基本构造是函数。 下面是将 1 添加到整数的简单函数:
let addOne x = x + 1
其类型签名如下所示:
val addOne: x:int -> int
签名可以读作“addOne
接受名为int
的x
,并生成一个int
”。 更正式地说,addOne
是将一个值从整数集映射到整数集。 令牌 ->
表示此映射。 在 F# 中,通常可以查看函数签名来了解其用途。
那么,为什么签名很重要? 在类型化函数编程中,函数的实现通常不如实际类型签名重要! addOne
向整数添加值 1 在运行时很有趣,但在构造程序时,它接受并返回int
是决定你实际如何使用此函数的关键。 此外,一旦你正确使用此函数(根据其类型签名),任何问题的诊断只能在 addOne
函数的主体内进行。 这是类型化函数编程背后的动力。
表达式
表达式是计算出一个值的结构。 与执行操作的语句相比,表达式可以被认为是执行一个返回值操作的表达形式。 表达式几乎始终用于函数编程而不是语句。
请考虑上一个函数。 addOne
addOne
正文是一个表达式:
// 'x + 1' is an expression!
let addOne x = x + 1
是此表达式的结果定义了 addOne
函数的结果类型。 例如,构成此函数的表达式可以更改为不同类型的,例如 string
:
let addOne x = x.ToString() + "1"
函数的签名现在为:
val addOne: x:'a -> string
由于 F# 中的任何类型都可以被 ToString()
调用,x
被泛型化(称为 自动通用化),因此结果类型是 string
。
表达式不仅仅是函数的主体。 可以使用表达式来生成在其他位置使用的值。 一个常见的是 if
:
// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0
let addOneIfOdd input =
let result =
if isOdd input then
input + 1
else
input
result
if
表达式将生成一个名为 result
的值。 请注意,可以完全省略 result
表达式,使 if
表达式成为函数的 addOneIfOdd
主体。 关键要记住的是,表达式会生成一个值。
有一种特殊类型, unit
在没有任何可返回内容时使用。 例如,请考虑以下简单函数:
let printString (str: string) =
printfn $"String is: {str}"
签名如下所示:
val printString: str:string -> unit
unit
类型指示没有返回实际值。 这在尽管工作没有返回任何值,但通常必须“工作”的情况下非常有用。
这与命令性编程形成鲜明对比,其中等效 if
构造是一个语句,生成值通常是使用可变变量完成的。 例如,在 C# 中,代码可能如下所示:
bool IsOdd(int x) => x % 2 != 0;
int AddOneIfOdd(int input)
{
var result = input;
if (IsOdd(input))
{
result = input + 1;
}
return result;
}
值得注意的是,C# 和其他 C 样式语言确实支持 三元表达式,这允许基于表达式的条件编程。
在函数编程中,很少使用语句来改变值。 尽管某些功能语言支持语句和突变,但在函数编程中使用这些概念并不常见。
纯函数
如前所述,纯函数是以下函数:
- 相同输入的计算结果始终为相同的值。
- 没有副作用。
在此上下文中考虑数学函数会很有帮助。 在数学中,函数只依赖于其参数,并且没有任何副作用。 在数学函数 f(x) = x + 1
中,值 f(x)
仅取决于其 x
值。 函数编程中的纯函数是相同的。
编写纯函数时,该函数必须仅依赖于其参数,并且不执行导致副作用的任何作。
下面是非纯函数的示例,因为它依赖于全局可变状态:
let mutable value = 1
let addOneToValue x = x + value
该 addOneToValue
函数显然不纯,因为 value
可以随时更改为与 1 不同的值。 在函数编程中,应避免这种依赖于全局值的模式。
下面是非纯函数的另一个示例,因为它执行副作用:
let addOneToValue x =
printfn $"x is %d{x}"
x + 1
尽管此函数不依赖于全局值,但它会将该值 x
写入程序的输出。 虽然这样做本质上没有任何问题,但它确实意味着函数不是纯粹的。 如果程序的另一部分依赖于程序外部的内容(如输出缓冲区),则调用此函数可能会影响程序的其他部分。
删除 printfn
语句会使函数纯化:
let addOneToValue x = x + 1
尽管此函数本身并不 优于 含有 printfn
语句的先前版本,但它确实保证该函数所做的一切都只是返回一个值。 调用此函数的任何次数都会生成相同的结果:它只生成一个值。 纯粹性带来的可预测性是许多函数式程序员所追求的。
不可变性
最后,类型化函数编程的最基本概念之一是不可变性。 在 F# 中,默认情况下,所有值都是不可变的。 这意味着,除非显式将它们标记为可变,否则无法就地转变它们。
在实践中,使用不可变值意味着将方法从“我需要更改某些内容”更改为“我需要生成新值”。
例如,将 1 添加到值意味着生成新值,而不是改变现有值:
let value = 1
let secondValue = value + 1
在 F# 中,以下代码 不会 改变 value
函数;而是执行相等性检查:
let value = 1
value = value + 1 // Produces a 'bool' value!
某些功能编程语言根本不支持突变。 在 F# 中,它受支持,但它不是值的默认行为。
此概念进一步扩展到数据结构。 在函数编程中,不可变的数据结构(如集(和更多)具有与最初预期不同的实现。 从概念上讲,向集添加项之类的内容不会更改集,它会生成具有附加值 的新 集。 在这种情况下,这通常是通过不同的数据结构来完成的,该结构允许有效地跟踪值,从而可以为数据提供适当的表示形式。
这种处理值和数据结构的方法至关重要,因为它迫使你在面对任何修改操作时,都要假设它会创建该事物的新版本。 这样,平等和可比性等内容就可以在程序中保持一致。
后续步骤
下一部分将全面介绍函数,探索可在函数编程中使用的不同方式。
在 F# 中使用函数 可深入探索函数,演示如何在各种上下文中使用它们。
延伸阅读
函数式思维系列是学习用 F# 进行函数式编程的另一个很好的资源。 它以务实且易于阅读的方式介绍了函数编程的基础知识,使用 F# 功能来说明概念。