Four Key Concepts

https://swlaschin.gitbooks.io/fsharpforfunandprofit/content/posts/key-concepts.html

四个关键概念

在接下来的几篇文章中,我们将继续展示本系列的主题:简洁、方便、正确、并发和完整性。

但在此之前,让我们看看 F# 中我们将反复遇到的一些关键概念。 F# 在很多方面都不同于 C# 等标准命令式语言,但有一些主要差异对于理解尤为重要:

  • Function-oriented rather than object-oriented

  • 面向函数而非面向对象

  • Expressions rather than statements

  • 表达式而不是语句

  • Algebraic types for creating domain models

  • 使用代数类型来创建领域模型

  • Pattern matching for flow of control

  • 使用模式匹配来进行流控制

在后面的文章中,这些内容将得到更深入的处理——这只是一个帮助您理解本系列其余部分的尝试。

four key concepts

面向函数而非面向对象

正如您对术语“函数式编程”所期望的那样,函数在 F# 中无处不在。

当然,函数是第一等级的实体,可以像其他值一样被传递:

let square x = x * x

// functions as values
let squareclone = square
let result = [1..10] |> List.map squareclone

// functions taking other functions as parameters
let execFunction aFunc aParam = aFunc aParam
let result2 = execFunction square 12

但是 C# 也有第一等级的函数,那么函数式编程有什么特别之处呢?

简短的回答是,F# 的面向函数的特性以一种在 C# 中没有的方式渗透到语言和类型系统的每个部分,因此在 C# 中笨拙或奇怪的东西在 F# 中非常优雅。

很难用几段话来解释这一点,但以下是我们将在这一系列帖子中看到的一些好处:

  • 组合式构建 组合是使我们能够从较小的系统构建较大系统的“粘合剂”。这不是一个可选的技术,而是函数式风格的核心。几乎每一行代码都是一个可组合的表达式(见下文)。组合用于构建基本函数,然后是使用这些函数的函数,等等。组合原则不仅适用于函数,也适用于类型(下面讨论的product和sum type)。

  • 分解与重构. 将一个问题分解成若干部分的能力取决于这些部分是否容易被重新粘在一起。在命令式语言中看似不可分割的方法和类,在函数式设计中往往可以被分解成令人惊讶的小块。这些细粒度的组件通常包括:(a)一些非常通用的函数,这些函数以其他函数为参数;(b)其他辅助函数,这些函数针对特定的数据结构或应用对通用情况进行了专业化处理。一旦派生出来,一般化的函数允许许多额外的操作被非常容易地编程,而不需要编写新的代码。你可以在关于从循环中提取重复代码的文章中看到这样一个通用函数的好例子(fold函数)。【TODO:替换链接】

  • Good design. Many of the principles of good design, such as "separation of concerns", "single responsibility principle", "program to an interface, not an implementation", arise naturally as a result of a functional approach. And functional code tends to be high level and declarative in general.

  • 良好的设计。许多好的设计原则,如 "关注点分离"、"单一责任原则"、"对接口编程,而不是对实现编程",都是由于功能化方法而自然产生。而函数式代码一般都倾向于高水平和声明性。【TODO:替换链接】

本系列的后续帖子将举例说明函数如何使代码更加简洁和方便,然后为了更深入的理解,还有一个关于函数式思考的系列

表达式而不是语句

在函数式语言中,没有语句,只有表达式。也就是说,每块代码都会返回一个值,而且更大的代码块是通过使用组合来创建的,而不是通过序列化的语句列表。

如果你使用过LINQ或SQL,你就已经熟悉了基于表达式的语言。例如,在纯SQL中,你不能有赋值。相反,你必须在较大的查询中拥有子查询。

SELECT EmployeeName 
FROM Employees
WHERE EmployeeID IN 
    (SELECT DISTINCT ManagerID FROM Employees)  -- subquery

F# 的工作方式相同——每个函数定义都是一个表达式,而不是一组语句。

它可能并不明显,但是从表达式构建的代码比使用语句更安全、更紧凑。为了解这一点,让我们将 C# 中一些基于语句的代码与等效的基于表达式的代码进行比较。

首先,基于语句的代码。语句不返回值,因此您必须使用从语句体内分配给的临时变量。

// statement-based code in C#
int result;     
if (aBool)
{
  result = 42; 
}
Console.WriteLine("result={0}", result);

因为 if-then 块是一个语句,result 变量必须在语句外定义,但从语句内赋值,这会导致以下问题:

  • result 初始值应该设置为什么?

  • 如果我忘记分配给variable变量怎么办?

  • “else”情况下variable变量的值是多少?

为了比较,这里是相同的代码,以面向表达式的风格重写:

// expression-based code in C#
int result = (aBool) ? 42 : 0;
Console.WriteLine("result={0}", result);

在面向表达的版本中,这些问题都不存在:

  • result变量在赋值的同时被声明。不必在表达式“外部”设置变量,也不必担心应将它们设置为什么初始值。

  • else”被显式处理。不可能存在忘记处理“else”的情况。

  • 不可能忘记分配result的值,因为那样变量就不存在!

面向表达式的风格不是 F# 中的一种选择,它是对于来自命令式背景的使用者时需要改变思考方式的事情之一。

代数类型

F# 中的类型系统基于代数类型的概念。也就是说,新的复合类型是通过以两种不同的方式组合现有类型来构建的:

  • 首先,值的组合,每个值都是从一组类型中挑选出来的。这些被称为“product,乘”类型。

  • 或者,作为表示一组类型之间的选择的不相交联合。这些被称为“sum,和”类型。

例如,给定现有类型 int 和 bool,我们可以创建一个新的类型,该类型必须具有以下任一类型:

//declare it
type IntAndBool = {intPart: int; boolPart: bool}

//use it
let x = {intPart=1; boolPart=false}

或者,我们可以创建一个新的 union/sum 类型,它可以在每种类型之间进行选择:

//declare it
type IntOrBool = 
    | IntChoice of int
    | BoolChoice of bool

//use it
let y = IntChoice 42
let z = BoolChoice true

这些 "Choice"类型在C#中是不可用的,但对于许多现实世界的案例的建模是非常有用的,比如状态机中的状态(这在许多领域是一个令人惊讶的常见主题)。

通过以这种方式组合“product”和“sum”类型,可以轻松创建一组丰富的类型来准确地为任何业务领域建模。有关此操作的示例,请参阅有关低开销类型定义使用类型系统确保代码正确的帖子。【todo:替换链接】

控制流的模式匹配

大多数命令式语言为分支和循环提供了各种控制流语句:

  • if-then-else(和三元版本 bool ? if-true : if-false)

  • case 或 switch 语句

  • for 和 foreach 循环,带 break 和 continue

  • while 和 until 循环

  • 甚至是可怕的 goto

F#确实支持其中的一些,但F#也支持条件表达式的最一般形式,即模式匹配

替换 if-then-else 的典型匹配表达式如下所示:

match booleanExpression with
| true -> // true branch
| false -> // false branch

switch 的替换可能是这样的:

match aDigit with
| 1 -> // Case when digit=1
| 2 -> // Case when digit=2
| _ -> // Case otherwise

最后,循环通常使用递归完成,通常看起来像这样:

match aList with
| [] -> 
     // Empty case 
| first::rest -> 
     // Case with at least one element.
     // Process first element, and then call 
     // recursively with the rest of the list

虽然 match 表达式起初看起来复杂得没必要,但您会发现在实践中它既优雅又强大。

For the benefits of pattern matching, see the post on exhaustive pattern matching, and for a worked example that uses pattern matching heavily, see the roman numerals example.

有关模式匹配的好处,请参阅有关详尽模式匹配的帖子,有关大量使用模式匹配的有效示例,请参阅罗马数字示例。【todo:替换链接】

对联合(Union)类型进行模式匹配

We mentioned above that F# supports a "union" or "choice" type. This is used instead of inheritance to work with different variants of an underlying type. Pattern matching works seamlessly with these types to create a flow of control for each choice.

我们在上面提到,F#支持 "Union"或 "choice"类型。它被用来代替继承,以处理一个底层类型的不同变体。模式匹配可以与这些类型无缝衔接,为每个choice创建一个控制流。

在下面的示例中,我们创建了一个代表四种不同形状的 Shape 类型,然后为每种形状定义了一个具有不同行为的绘制函数。这类似于面向对象语言中的多态性,但基于函数。

type Shape =        // define a "union" of alternative structures
| Circle of int 
| Rectangle of int * int
| Polygon of (int * int) list
| Point of (int * int) 

let draw shape =    // define a function "draw" with a shape param
  match shape with
  | Circle radius -> 
      printfn "The circle has a radius of %d" radius
  | Rectangle (height,width) -> 
      printfn "The rectangle is %d high by %d wide" height width
  | Polygon points -> 
      printfn "The polygon is made of these points %A" points
  | _ -> printfn "I don't recognize this shape"

let circle = Circle(10)
let rect = Rectangle(4,5)
let polygon = Polygon( [(1,1); (2,2); (3,3)])
let point = Point(2,3)

[circle; rect; polygon; point] |> List.iter draw

有几点需要注意:

  • 像往常一样,我们不必指定任何类型。编译器正确地确定了“draw”函数的形状参数是形状类型。

  • 您可以看到 match..with 逻辑不仅匹配形状的内部结构,而且还根据适合形状的内容分配值。

  • 下划线类似于switch语句中的 "默认 "分支,只是在F#中它是必须的--每一种可能的情况都必须被处理。如果你注释掉这一行

    | _ -> printfn "I don't recognize this shape"

看看编译时会发生什么!

在C#中可以通过使用子类或接口来模拟这类选择类型,但C#类型系统中没有内置支持这种带有错误检查的详尽匹配。

Last updated