Skip to content

Latest commit

 

History

History
377 lines (253 loc) · 20 KB

04.md

File metadata and controls

377 lines (253 loc) · 20 KB

四、类型和类型推断

F# 是一种强类型语言,这意味着你不能使用一个值不合适的函数。不能调用以字符串作为参数并带有整数参数的函数;您必须在两者之间显式转换。这种语言对待其价值类型的方式被称为其类型系统。F# 有一个不会妨碍常规编程的类型系统。在 F# 中,所有值都有一个类型,这包括作为函数的值。

类型推断

通常,您不需要显式声明类型;编译器将从函数中文字的类型和它调用的其他函数的结果类型中计算出一个值的类型。如果一切正常,编译器会将类型保留给自己;只有当存在类型不匹配时,编译器才会通过报告编译错误来通知您。这个过程一般被称为式推理。如果你想更多地了解程序中的类型,可以用–i开关让编译器显示所有推断的类型。Visual Studio 用户将鼠标指针悬停在标识符上时,会获得显示类型的工具提示。

类型推断在 F# 中的工作方式相当容易理解。编译器在整个程序中工作,在定义标识符时为它们分配类型,从最左边的标识符开始,一直向下到最右边的标识符。它根据已经知道的类型分配类型,也就是文字的类型和(更常见的)在其他源文件或程序集中定义的函数的类型。

下一个例子定义了两个 F# 标识符,然后用 F# 编译器的–i开关在控制台上显示它们的推断类型。

    let aString = "Spring time in Paris"
    let anInt = 42

下垂 aString : string

val anInt : int

这两个标识符的类型并不令人惊讶——分别是stringint。编译器用来描述它们的语法相当简单:关键字val(意思是“值”),然后是标识符、冒号,最后是类型。

下一个例子中函数makeMessage的定义稍微有趣一点。

    let makeMessage x = (Printf.sprintf "%i" x) + " days to spring time"
    let half x = x / 2

val makeMessage : int ->字符串

val half : int -> int

注意makeMessage函数的定义前缀是关键字val,就像你之前看到的两个值一样;即使它是一个函数,F# 编译器仍然认为它是一个值。此外,类型本身使用符号int -> string,这意味着一个函数接受一个整数并返回一个字符串。类型名称之间的-> (ASCII 箭头)表示正在应用的函数的转换。箭头表示值的转换,但不一定表示类型,因为它可以表示将值转换为相同类型值的函数,如第二行的half函数所示。

可以部分应用的函数类型和接受元组的函数类型不同。以下功能div1div2说明了这一点。

    let div1 x y = x / y
    let div2 (x, y) = x / y

    let divRemainder x y = x / y, x % y

val div1 : 你 -> 你 -> 你是

val div2 : int*you -> you

val divremainder:int-> int-> int * int

函数div1可以部分应用,类型为int -> int -> int,表示参数可以单独传入。将它与类型为“T3”的函数“T2”进行比较,T3 是一个取一对整数——一组整数——并将它们转化为一个整数的函数。您可以在函数div_remainder中看到这一点,该函数执行整数除法,同时还返回余数。它的类型是int -> int -> int * int,意思是返回整数元组的 curried 函数。

下一个函数doNothing,看起来足够不显眼,但是从打字的角度来看还是挺有意思的。

    let doNothing x = x

瓦尔·多诺西:“a->”a

该函数的类型为'a -> 'a,即取一个类型的值,返回一个相同类型的值。任何以单引号(')开头的类型都意味着一个变量类型。F# 有一个类型obj,它映射到System.Object并代表任何类型的值——这个概念你可能会从其他基于公共语言运行时(CLR)的编程语言中熟悉(事实上,许多语言并不以 CLR 为目标)。但是,变量类型不一样。请注意该类型在箭头的两侧有一个'a。这意味着,即使编译器还不知道类型,它也知道返回值的类型将与参数的类型相同。类型系统的这一特性,有时被称为类型参数化,允许编译器在编译时发现更多的类型错误,并有助于避免强制转换。

| | 注意:变量类型或类型参数化的概念与泛型的概念密切相关,后者是在 CLR 2.0 版本中引入的,现在已经成为 CLI 2.0 版本的 ECMA 规范的一部分。当 F# 以启用了泛型的 CLI 为目标时,它会通过在找到未确定类型的任何地方使用它们来充分利用它们。F# 的创建者 Don Syme 在。NET CLR 在他开始研究 F# 之前。有人可能会想推断他这么做是为了创造 F#! |

下一个示例中显示的函数doNothingToAnInt是一个被约束的值的例子——一个类型的约束。在这种情况下,功能参数x被约束为int。可以将任何标识符约束为某种类型,而不仅仅是函数参数,尽管更典型的是需要约束参数。这里的列表stringList显示了如何约束不是函数参数的标识符。

    let doNothingToAnInt (x: int) = x
    let intList = [1; 2; 3]

    let (stringList: list<string>) = ["one"; "two"; "three"]

val donothingoanint _ int:int-> int

选择 intList : int list

val stringList:字符串列表

将值约束为特定类型的语法很简单。在括号内,标识符名称后面是冒号(:),后面是类型名称。这有时也被称为式注释

intList值是整数列表,标识符的类型是int list。这表明编译器已经识别出列表只包含整数,在这种情况下,其项目的类型不是未确定的,而是int。除了类型为int的值之外,任何向列表中添加任何内容的尝试都将导致编译错误。

标识符stringList有类型标注。虽然这是不必要的,因为编译器可以从值解析类型,但它用于显示处理未确定类型的替代语法。您可以将该类型放在与其关联的类型之后的尖括号之间,而不只是将其写在类型名称之前。请注意,即使stringList的类型被限制为list<string>(字符串列表),编译器在显示类型时仍然将其类型报告为string list,它们的含义完全相同。支持此语法以使带有类型参数的 F# 类型看起来像其他类型的泛型类型。NET 库。

在编写纯 F# 时,约束值通常不是必需的,尽管它偶尔会有用。使用时最有用。NET 库,用 F# 以外的语言编写,用于与非托管库进行互操作。在这两种情况下,编译器的类型信息较少,因此通常需要给它足够的信息来消除值的歧义。

定义类型

F# 中的类型系统提供了许多定义自定义类型的功能。所有 F# 的类型定义都分为两类:

  • 元组记录,它们是组成复合类型的一组类型(类似于 C 中的结构或 C#中的类)。
  • 求和类型,有时称为联合类型。

元组和记录类型

元组是一种快速方便地将值组成一组值的方法。值由逗号分隔,然后可以由一个标识符引用,如下一个示例的第一行所示。然后,您可以通过执行相反的操作来检索值,如第二行和第三行所示,其中由逗号分隔的标识符出现在等号的左侧,每个标识符从元组中接收一个值。如果你想忽略元组中的一个值,可以用_告诉编译器你对这个值不感兴趣,如第二、三行所示。

    let pair = true, false
    let b1, _ = pair
    let _, b2 = pair

元组不同于 F# 中大多数用户定义的类型,因为您不需要使用type关键字显式声明它们。要定义类型,您可以使用type关键字,后跟类型名称、等号,然后是您正在定义的类型。在最简单的形式中,您可以使用它为任何现有类型(包括元组)提供别名。给单个类型赋予别名通常并不有用,但是给元组赋予别名可能非常有用,尤其是当您想要使用元组作为类型约束时。下一个示例显示了如何为单个类型和元组赋予别名,以及如何使用别名作为类型约束。

    type Name = string
    type Fullname = string * string

    let fullNameToSting (x: Fullname) =
        let first, second = x in
        first + " " + second

记录类型类似于元组,因为它们将多个类型组成一个类型。不同的是,在记录类型中,每个字段都被命名。下一个示例说明了定义记录类型的语法。

    // Define an organization with unique fields.
    type Organization1 = { boss: string; lackeys: string list }
    // Create an instance of this organization.
    let rainbow =
        { boss = "Jeffrey";
          lackeys = ["Zippy"; "George"; "Bungle"] }

    // Define two organizations with overlapping fields.
    type Organization2 = { chief: string; underlings: string list }
    type Organization3 = { chief: string; indians: string list }

    // Create an instance of Organization2.
    let (thePlayers: Organization2) =
        { chief = "Peter Quince";
          underlings = ["Francis Flute"; "Robin Starveling";
                        "Tom Snout"; "Snug"; "Nick Bottom"] }

    // Create an instance of Organization3.
    let (wayneManor: Organization3) =
        { chief = "Batman";
          indians = ["Robin"; "Alfred"] }

将字段定义放在大括号之间,并用分号分隔。字段定义由后跟冒号的字段名称和字段类型组成。类型定义Organization1是字段名称唯一的记录类型。这意味着您可以使用简单的语法来创建这种类型的实例,在创建时不需要提及类型名称。要创建记录,您需要将字段名称后跟等号,并将字段值放在大括号({})之间,如Rainbow标识符所示。

F# 不强制字段名是唯一的,所以有时编译器不能仅从字段名推断字段的类型。在这种情况下,编译器无法推断记录的类型。要创建具有非唯一字段的记录,编译器需要静态地知道正在创建的记录的类型。如果编译器无法推断记录的类型,您需要使用类型注释,如类型推断部分所述。使用类型注释由类型Organization2Organization3以及它们的实例thePlayerswayneManor来说明。您可以看到在其名称后面显式给出的标识符的类型。

访问记录中的字段相当简单。您只需使用语法record标识符名称,后跟一个点,后跟字段名称。下面的例子说明了这一点,展示了如何访问Organization记录的chief字段。

    // Define an organization type.
    type Organization = { chief: string; indians: string list }

    // Create an instance of this type.
    let wayneManor =
        { chief = "Batman";
          indians = ["Robin"; "Alfred"] }

    // Access a field from this type.
    printfn "wayneManor.chief = %s" wayneManor.chief

默认情况下,记录是不可变的。对于一个命令式程序员来说,这听起来好像记录不是很有用,因为不可避免地会出现需要更改字段值的情况。为此,F# 提供了一种简单的语法,用于创建具有更新字段的记录副本。要创建记录的副本,请将该记录的名称放在大括号中,后跟关键字with,然后是要用其更新值进行更改的字段列表。这样做的好处是,您不需要重新键入未更改的字段列表。下面的示例演示了这种方法。它创建了wayneManor的初始版本,然后创建了wayneManor',其中"Robin"已经被移除。

    // Define an organization type.
    type Organization = { chief: string; indians: string list }

    // Create an instance of this type.
    let wayneManor =
        { chief = "Batman";
          indians = ["Robin"; "Alfred"] }

    // Create a modified instance of this type.
    let wayneManor' =
        { wayneManor with indians = [ "Alfred" ] }

    // Print out the two organizations.
    printfn "wayneManor = %A" wayneManor
    printfn "wayneManor' = %A" wayneManor'

编译和执行该示例的结果如下:

wayneManor = { chief = "蝙蝠侠";

印第安人= [“罗宾”;“阿尔弗雷德”];}

wayneManor ' = { chief = "蝙蝠侠";

印第安人= [“阿尔弗雷德”];}

访问记录中字段的另一种方法是使用模式匹配;也就是说,您可以使用模式匹配来匹配记录类型中的字段。如您所料,使用模式匹配检查记录的语法类似于构造记录的语法。您可以使用字段 = 常量将字段与常量进行比较。可以用字段 = 标识符来分配带有标识符的字段的值。可以忽略带有字段 =的字段。以下示例中的findDavid函数说明了如何使用模式匹配来访问记录中的字段。

    // Type representing a couple.
    type Couple = { him : string ; her : string }

    // List of couples.
    let couples =
        [ { him = "Brad" ; her = "Angelina" };
          { him = "Becks" ; her = "Posh" };
          { him = "Chris" ; her = "Gwyneth" };
          { him = "Michael" ; her = "Catherine" } ]

    // Function to find "David" from a list of couples.
    let rec findDavid l =
        match l with
        | { him = x ; her = "Posh" } :: tail -> x
        | _ :: tail -> findDavid tail
        | [] -> failwith "Couldn't find David"

    // Print the results.
    printfn "%A" (findDavid couples)

findDavid功能中的第一条规则是做真正工作的规则,检查记录的her字段,看是否是大卫的妻子"Posh"him字段与标识符x相关联,因此可以在规则的后半部分使用。

编译和执行该示例的结果如下:

贝克斯

需要注意的是,当像这样对记录进行模式匹配时,只能使用文字值。因此,如果您想要概括该函数以允许您更改正在搜索的人,您需要在模式匹配中使用when守卫:

    let rec findPartner soughtHer l =
        match l with
        | { him = x ; her = her } :: tail when her = soughtHer -> x
        | _ :: tail -> findPartner soughtHer tail
        | [] -> failwith "Couldn't find him"

    // Print the results.
    printfn "%A" (findPartner "Angelina" couples )

字段值也可以是函数,它偶尔可以用来提供类似对象的行为,因为记录的每个记录实例都可以有不同的函数实现。

联合或求和类型

联合类型,有时称为和类型区分联合,是一种将可能具有不同含义或结构的数据聚集在一起的方式。

您可以使用type关键字定义一个联合类型,后跟类型名称和等号,与所有类型定义相同。接下来是由管道分隔的不同构造器的定义。第一个管道是可选的。

构造函数由必须以大写字母开头的名称组成,这是为了避免构造函数名称与标识符名称混淆的常见错误。该名称后面可以有关键字of,然后是构成该构造函数的类型。组成构造函数的多个类型用星号隔开。类型中构造函数的名称必须是唯一的。如果定义了几个联合类型,那么它们的构造函数的名称可以重叠;但是,这样做时应该小心,因为在构造和使用联合类型时可能需要更多的类型注释。

下一个例子定义了一种类型Volume,其值可以有三种不同的含义:升、美国品脱或英制品脱。虽然数据的结构是相同的,并由一个浮点数表示,但含义却大不相同。在算法中混淆数据的含义是导致程序错误的常见原因,而Volume类型在某种程度上是为了避免这种情况。

    type Volume =
        | Liter of float
        | UsPint of float
        | ImperialPint of float

    let vol1 = Liter 2.5
    let vol2 = UsPint 2.5
    let vol3 = ImperialPint (2.5)

构造联合类型的新实例的语法是构造函数名,后跟类型的值,多个值用逗号分隔。或者,您可以将值放在括号中。您可以使用三个不同的Volume构造函数来构造三个不同的标识符:vol1vol2vol3

要将联合类型的值分解为它们的基本部分,您总是使用模式匹配。当在联合类型上进行模式匹配时,构造函数构成了模式匹配规则的前半部分。您不需要完整的规则列表,但是如果列表不完整,则必须有一个使用标识符或通配符来匹配所有剩余规则的默认规则。构造函数规则的第一部分包括构造函数名称,后跟标识符或通配符,以匹配其中的各种值。以下convertVolumeToLiterconvertVolumeUsPintconvertVolumeImperialPint函数演示了该语法:

    // Type representing volumes.
    type Volume =
        | Liter of float
        | UsPint of float
        | ImperialPint of float

    // Various kinds of volumes.
    let vol1 = Liter 2.5
    let vol2 = UsPint 2.5
    let vol3 = ImperialPint 2.5

    // Some functions to convert between volumes.
    let convertVolumeToLiter x =
        match x with
        | Liter x -> x
        | UsPint x -> x * 0.473
        | ImperialPint x -> x * 0.568
    let convertVolumeUsPint x =
        match x with
        | Liter x -> x * 2.113
        | UsPint x -> x
        | ImperialPint x -> x * 1.201
    let convertVolumeImperialPint x =
        match x with
        | Liter x -> x * 1.760
        | UsPint x -> x * 0.833
        | ImperialPint x -> x

    // A function to print a volume.
    let printVolumes x =
        printfn "Volume in liters = %f,
    in us pints = %f,
    in imperial pints = %f"
            (convertVolumeToLiter x)
            (convertVolumeUsPint x)
            (convertVolumeImperialPint x)
    // Print the results.
    printVolumes vol1
    printVolumes vol2
    printVolumes vol3

这个问题的另一个解决方案是使用 F# 的度量单位,它允许将类型应用于数值。

带类型参数的类型定义

联合类型和记录类型都可以参数化。参数化一个类型意味着将该类型中的一个或多个类型留给该类型的使用者来定义。这是与本章前面讨论的变量类型类似的概念。定义类型时,您必须更加明确哪些类型是可变的。

要创建一个或多个类型参数,请将参数化的类型放在类型名称后面的尖括号中,如下所示:

    type BinaryTree<'a> =
    | BinaryNode of 'a BinaryTree * 'a BinaryTree
    | BinaryValue of 'a

    let tree1 =
        BinaryNode(
            BinaryNode ( BinaryValue 1, BinaryValue 2),
            BinaryNode ( BinaryValue 3, BinaryValue 4) )

与变量类型一样,类型参数的名称总是以单引号(')开头,后跟该类型的字母数字名称。通常,只使用一个字母。如果需要多个参数化类型,可以用逗号分隔它们。然后,您可以在整个类型定义中使用类型参数。

创建和使用参数化类型实例的语法与创建和使用非参数化类型的语法没有变化。这是因为编译器会自动推断参数化类型的类型参数。在tree1的如下构造中可以看到这一点,以及它们被printBinaryTreeValues函数消耗的情况:

    // Definition of a binary tree.
    type BinaryTree<'a> =
        | BinaryNode of 'a BinaryTree * 'a BinaryTree
        | BinaryValue of 'a

    // Create an instance of a binary tree.
    let tree1 =
        BinaryNode(
            BinaryNode ( BinaryValue 1, BinaryValue 2),
            BinaryNode ( BinaryValue 3, BinaryValue 4) )

    // Function to print the binary tree.
    let rec printBinaryTreeValues x =
        match x with
        | BinaryNode (node1, node2) ->
            printBinaryTreeValues node1
            printBinaryTreeValues node2
        | BinaryValue x ->
            printf "%A, " x

    // Print the results.
    printBinaryTreeValues tree1

编译和执行该示例的结果如下:

1, 2, 3, 4,

您可能已经注意到,虽然我已经讨论了定义类型、创建类型的实例以及检查这些实例,但是我还没有讨论更新它们。不可能更新这些类型,因为值随时间变化的想法与函数式编程的想法背道而驰。

总结

这是对如何在 F# 中创建类型的简单介绍。你会发现 F# 的类型系统提供了一种灵活的方式来表示程序中的数据。