Facebook Twitter LinkedIn E-mail
magnify
formats

F#开发教程(9):函数化编程(八)

可区分联合

可区分联合(Discriminated Unions),简称为联合,是一种可以将多个不同意义和结构的数据组合在一起的方法,这点有点和C语言的联合类型相似。同样适用type来定义一个联合类型(F#中自定义类型基本上除元组外都是适用type来定义)。
联合以type关键字开始,然后适用“=” 来定义联合,等好后面使用“|”来定义不同类型的构造器。
构造器通常以大写字母起始的名字开始,构造器需要缩进以避免和标志符混淆。名字后面可以跟可选的数据类型,多个类型之间使用“*”分隔,例如:

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

type Shape =
| Square of side:float
| Rectangle of width:float * height:float
| Circle of radius:float

// create an instance of a union type without using the field label
let sq = Square 1.2
// create an instance of a union type using the field label
let sq2 = Square(side=1.2)
// create an instance of a union type using multiple field labels
// and assigning the field out-of-order
let rect3 = Rectangle(height=3.4, width=1.2)

可以使用模式匹配来拆分联合的各个组成部分。当使用模式匹配来匹配一个联合类型时 ,规则的第一部分为联合成员的构造器定义,你不要为每个子类型都定义匹配规则,但如果规则没有包含所有子类型的话,需要定义一个通配规则来匹配余下的子类型。

// 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


显示结果为

Volume in liters = 2.500000,
in us pints = 5.282500,
in imperial pints = 4.400000
Volume in liters = 1.182500,
in us pints = 2.500000,
in imperial pints = 2.082500
Volume in liters = 1.420000,
in us pints = 3.002500,
in imperial pints = 2.500000

类型定义和类型参数

联合类型和记录类型都支持类型参数化,参数化一个类型意味着暂时不定义联合或记录类型中某些子类型,而是在使用这些类型时在决定哪些定义时未定的子类型。
F#支持两种形式的类型参数化,第一种形式,你把参数化类型放在type关键字和类型名称之间:

type 'a BinaryTree =
| BinaryNode of 'a BinaryTree * 'a BinaryTree
| BinaryValue of 'a
let tree1 =
    BinaryNode(
        BinaryNode ( BinaryValue 1, BinaryValue 2),
        BinaryNode ( BinaryValue 3, BinaryValue 4) )

第二种形式是使用将参数放在尖括号。

type Tree<'a> =
| Node of Tree<'a> list
| Value of 'a
let tree2 =
    Node( [ Node( [Value "one"; Value "two"] ) ;
         Node( [Value "three"; Value "four"] ) ]  )

所有的类型参数都以’开始,然后为字母数字名称,类型参数通常使用单字母来表示,多个类型参数使用逗号分开。
F#类型参数的作用和其它语言比如C#的generic(通用类型或泛型)类似。
在使用包含类型参数的类型时,由于F#编译器强大的类型推断功能,用法和不含类型参数的类型的用法基本上相同,F#会根据上下文推断出类型参数代表的实际的数据类型,比如:

- type 'a BinaryTree =
-     | BinaryNode of 'a BinaryTree * 'a BinaryTree
-     | BinaryValue of 'a;;

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

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

val tree1 : int BinaryTree =
  BinaryNode
    (BinaryNode (BinaryValue 1,BinaryValue 2),
     BinaryNode (BinaryValue 3,BinaryValue 4))

可以看出F#编译器自动推断出tree1的中’a实际为int类型。

递归定义

前面的BinaryTree 在定义时,引用到了自身,也是一个递归定义,有些时候,在定义一个类型时,需要引用到后面定义的类型,通常情况我们没有办法引用尚未定义过的类型,即使它们定义在同一个文件中。某些情况下,定义在同一个文件中的两个不同类型可能需要相互引用。
F#中定义了一个特别的语法来接近类型的互相引用。两个具有互相引用的类型必须定义同一文件的同一代码块中,而且两个类型定义必须一个紧跟着一个,之间不能有其它类型定义,然后使用”and”关键字连接起来。
比如我们定义一个XML文件

// represents an XML attribute
type XmlAttribute =
    { AttribName: string;
      AttribValue: string; }
// represents an XML element
type XmlElement =
    { ElementName: string;
      Attributes: list<XmlAttribute>;
      InnerXml: XmlTree }
// represents an XML tree
and XmlTree =
  | Element of XmlElement
  | ElementList of list<XmlTree>
  | Text of string
  | Comment of string
  | Empty
 
Tags:
 Share on Facebook Share on Twitter Share on Reddit Share on LinkedIn
F#开发教程(9):函数化编程(八)已关闭评论  comments 
formats

F#开发教程(8):函数化编程(七)

类型和类型推断

F#为强类型定义语言,这意味着你不能使用类型不匹配的数据来调用函数。比如你不能使用整数数据来调用需要字符串类型参数的函数。如有必要,你必须强制转换类型。
但是F#编译器具有很强类型分析能力,可以根据上下文推断出函数或参数或是标志符的类型,因此一般来说你无需明确指明数据类型。这个过程称为类型推断(type inference)。有了类型推断,书写的代码会显得更加简洁。如有需要,你也可以指明数据类型,比如

&gt; let doNothing x = x;;

val doNothing : x:'a -&gt; 'a

这段代码中没有指明数据的类型,F#推断出该函数的类型为 ‘a -> ‘a ,这里单引号’代表一个通用类型,尽管F#不能确定a的具体类型,但它知道函数的参数类型和返回值得类型是一致的。因此使用同一个字母a来表示。

如果我们给定参数的类型

> let doNothingToAnInt (x:int) = x;;

val doNothingToAnInt : x:int -&gt; int

定义类型

F#提供了多种方法来定义自定义的数据类型,主要分为以下三种:

  • 元组类型或记录类型 由一组类型组成型的组合类型(有些类似C语言中的结构类型)
  • 联合类型union
  • 类结构class

 

元组或记录类型

元组定义使用逗号分隔元组中的元素。元组可以方便将多个数值组合在一起。

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

这个例子第一行定义了一个二元组,后面的三行用来读取二元组中的单个元素,可以使用位置匹配的方法来读取元素。如果你不关心某个元素,可以使用_忽略对该元素的读取。

元组的单个元素没有名称,如果你需要通过名称来访问组合类型,可以使用记录类型,记录类型使用type来定义,其实type可以用来定义集合除元组之外的所有自定义类型。元组类型的定义无需使用type关键字。type 可以定义多种数据类型,比如定义类型的别名

type Name = string
type Fullname = string * string

记录类型和元组类型非常类型,所不同是记录类型为每个元素给出了名字,比如

type Organization1 = { boss: string; lackeys: string list }
// create an instance of this organization
let rainbow =
    { boss = "Jeffrey";
      lackeys = ["Zippy"; "George"; "Bungle"] }

访问记录类型的成员,可以直接通过成员的名称来访问,比如

// 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来创建新的修改过的记录数据,比如

> type Organization = { chief: string; indians: string list }
- ;;  

type Organization =
  {chief: string;
   indians: string list;}

> let wayneManor =
-     { chief = "Batman";
-       indians = ["Robin"; "Alfred"] };;

val wayneManor : Organization = {chief = "Batman";
                                 indians = ["Robin"; "Alfred"];}

> let wayneManor' =
-     { wayneManor with indians = [ "Alfred" ] };;

val wayneManor' : Organization = {chief = "Batman";
                                  indians = ["Alfred"];}

除了使用.来访问记录成员外,你还可以使用模式匹配来访问记录类型。

// 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)

这段代码执行后,返回Becks. 它使用递归来实现循环查找列表中的某个记录。

 
Tags:
 Share on Facebook Share on Twitter Share on Reddit Share on LinkedIn
F#开发教程(8):函数化编程(七)已关闭评论  comments 
formats

F#开发教程(7):函数化编程(六)

列表List

针对列表的模式匹配
F#中通常使用模式匹配和递归来处理列表,模式匹配可以通过 head::tail来分离列表的头元素和表尾。比如下面函数用来把列表的列表中的元素展开

> let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]];;

val listOfList : int list list = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]

> listOfList |> concatLst;;
val it : int list = [2; 3; 5; 7; 11; 13; 17; 19; 23; 29]let twoLists = ["one, "; "two, "] @ ["buckle "; "my "; "shoe "]

针对列表的高阶函数

你可以使用递归和模式匹配来处理列表,通常情况下你无需这么做,F#定义用于集合的高阶函数,比如map, iter, fold,unfold,首先查看F#列表中有没有可用的高阶函数,比如,你可以自己定义下面的函数为列表中每个元素加1.

let rec addOneAll list =
    match list with
    | head :: rest ->
        head + 1 :: addOneAll rest
    | [] -> []

实际上,你可以使用 map 来实现,

> List.map ((+) 1) [1;2;3];;
val it : int list = [2; 3; 4]

是不是很简洁易懂:-).

列表解析

列表解析(原文是List comprehensions,找不到中文合适的词来翻译这个词组,解析比较接近原意)使得创建和转换集合类型的数据变得容易。你可以使用解析语法结构来创建列表(list),序列(Seq)和数组(array).
最简单的解析语法为指定一个区域,指定集合的初值和终值,以..分开。比如

let numericList = [ 0 .. 9 ]
let alpherSeq = seq { 'A' .. 'Z' }
let numerArray = [| 0 .. 9|]

如果你需要指定区域的步长,可以使用三个参数,(初值 .. 步长 .. 终值) 例如

> [ 0 .. 2 .. 20];;
val it : int list = [0; 2; 4; 6; 8; 10; 12; 14; 16; 18; 20]
> [| 0 .. 2 .. 20|];;
val it : int [] = [|0; 2; 4; 6; 8; 10; 12; 14; 16; 18; 20|]
> { 0 .. 2 .. 20};;
val it : seq<int> = seq [0; 2; 4; 6; ...]

列表解析也支持使用循环来创建集合,或者使用其它集合数据来创建新的集合。 比如

> let sequars = seq { for x in 1 .. 10 -> x * x };;

val sequars : seq<int>

使用yield 提供了在创建集合时有更大的灵活性,你可以根据需要决定哪些数值在集合中,比如
下面代码创建一个偶数序列。

> let even n = seq { for x in 1 .. n do if x % 2 =0 then yield x };; 

val even : n:int -> seq<int>

> even 10;;
val it : seq<int> = seq [2; 4; 6; 8; ...]
 
Tags:
 Share on Facebook Share on Twitter Share on Reddit Share on LinkedIn
F#开发教程(7):函数化编程(六)已关闭评论  comments 
formats

F#开发教程(6):函数化编程(五)

流程控制

F#支持很强的程序流程控制,这个纯函数化语言有些不同,纯粹的函数化语言的流程控制比较弱,也就是表达式的计算的顺序可以是任意的顺序。F#的强流程控制体现在if … then .. else 表达式上。
F# if … then .. else 结构也是一个表达式,也是有返回结果的。它返回某个分支的数值,具体取决于分支的条件。比如:

let result =
    if System.DateTime.Now.Second % 2 = 0 then "heads" else  "tails"
    

其实F#中if … then .. else 表达式也是模式匹配的简洁的表现形式,什么代码使用模式匹配可以重新写为:

let result =
    match System.DateTime.Now.Second % 2 = 0 with
    | true -> "heads"
    | false ->  "tails"

需要注意的F#要求if … then .. else 表达式的不同分支返回相同类型的数值。否则编译会给出警告。在某些情况下,你需要返回不同类型的数据,可以使用box 将它们转换成object。
例如

let result =
    if System.DateTime.Now.Second % 2 = 0 then
        box "heads"
    else
        box false

此外F#if … then .. else 表达式 必须包含else部分,这在其它语言看起来有些奇怪,但对于F#来说是符合逻辑的,如果去掉else,那么编译器在if条件不满足时,无法返回结果。

 
Tags:
 Share on Facebook Share on Twitter Share on Reddit Share on LinkedIn
F#开发教程(6):函数化编程(五)已关闭评论  comments 
formats

F#开发教程(5):函数化编程(四)

模式匹配

可以根据输入值的不同来决定使用不同的计算过程。这点有点类似其它语言中的switch语句。但F#中模式匹配功能要强大灵活得多。
函数化编程常常需要根据输入来编写一系列的数据变换。模式变换允许你分析输入数据,然后根据输入数据的模式来决定哪种变换,模式匹配非常适合于这种函数化编程风格。
F# 中模式匹配支持匹配输入值的数据类型,数值内容等等。
最简单的模式匹配是匹配单个数值,比如你之前看到的递归函数定义。
F#使用match来定义匹配,每个匹配规则使用|分隔。
比如下面的lucas数列

let rec luc x =
    match x with
    | x when x <= 0 -> failwith "value must be greater than 0"
    | 1 -> 1
    | 2 -> 3
    | x -> luc (x - 1) + luc (x - 2)

每个匹配规则的缩进需要统一。具体匹配规则时根据定义的顺序从上到下依次匹配。如果某个规则由于前面规则的定义永远匹配不到,编译器会给出警告。
规则可以使用when 来进一步约束规则匹配的条件,只有在同时满足匹配规则和约束条件时,该规则才会被触发。如果你需要一个通配规则,你可以使用_来定义一个通配规则。

let booleanToString x =
    match x with false -> "False" | _ -> "True"

上面的代码也演示F#模式匹配另外一个属性,可以使用|组合多个规则成一个规则。
类似的例子:

let stringToBoolean x =
    match x with
    | "True" | "true" -> true
    | "False" | "false" -> false
    | _ -> failwith "unexpected input"

模式匹配也支持匹配F#几乎所有的数据类型,你几乎可以使用你所有能想到的方式来匹配输入,比如:

let myOr b1 b2 =
    match b1, b2 with
    | true, _ -> true
    | _, true -> true
    | _ -> false

let myAnd p =
    match p with
    | true, true -> true
    | _ -> false

由于F#模式匹配使用得非常普遍,F#支持一种简化的表述方式。如果函数的主要作用是匹配某种某事,你可以在定义参数的地方使用function关键字来替代,然后使用|来分隔规则。例如:

> let rec concatStringList =                                           
-     function head :: tail -> head + concatStringList tail | [] -> "";;

val concatStringList : _arg1:string list -> string

> let jabber = ["'Twas "; "brillig, "; "and "; "the "; "slithy "; "toves "; "..."];;

val jabber : string list =
  ["'Twas "; "brillig, "; "and "; "the "; "slithy "; "toves "; "..."]

> concatStringList jabber;;
val it : string = "'Twas brillig, and the slithy toves ..."

模式匹配是F#中非常普遍的构造模块,我们后面将比较详细的进一步介绍它的用法。

 
Tags:
 Share on Facebook Share on Twitter Share on Reddit Share on LinkedIn
F#开发教程(5):函数化编程(四)已关闭评论  comments 
formats

F#开发教程(4):函数化编程(三)

函数调用

函数应用指调用函数,并传入参数。比如


let add x y = x + y
let result = add 4 5
printfn ("add 4 5) = %i" result

这个例子运行结果

(add 4 5) = 9

可以看出,F#调用函数是参数不需要使用括号和逗号分隔,而是使用空格来分隔参数。

F#也提供了另外的方法来调用函数,使用管道操作符 |>, 管道操作符定义如下:


let (|>) x f = f x

管道表示接受一个参数 x,然后应用到指定函数 f, 管道使得操作数在函数之前。例如 下面的例子 参数0.5 通过管道传递给cos函数

let result = 0.5 |> System.Math.Cos

函数名函数参数位置的变换在某些时候非常有用,特别是多个函数串联在一起时。比如


let add x y = x + y
let result = add 6 7 |> add 4 |> add 5

部分函数(偏函数应用)

F#支持函数的部分应用(柯里化或是偏函数应用),这意味着你在调用函数时无需一次性传入所有参数。比如前面例子 add 4 就是函数的部分调用。

F#函数也是一个值,如果调用时没有传入全部参数,那么该调用返回另外一个函数,参数为余下未传入的参数。这种行为有时不是特别适合某些情况,比如参数为x,y平面坐标,你不希望一次传入一个坐标,此时你可以使用(),此时(x,y)定义了一个元组类型的参数,而不是两个独立的参数。此时你必须传入一个二元组参数来调用该函数。


> let sub (a,b) = a - b;;

val sub : a:int * b:int -> int

> sub (3,4);;
val it : int = -1
> sub 4;;

  sub 4;;
  ----^

/Users/James/Workspace/stdin(6,5): error FS0001: This expression was expected to have type
    'int * int'    
but here has type
    'int'    

sub 4 出现编译错误是因为函数 sub 的参数为一个二元组,调用是需要传入一个二元组 int * int 类型。

 
Tags:
 Share on Facebook Share on Twitter Share on Reddit Share on LinkedIn
F#开发教程(4):函数化编程(三)已关闭评论  comments