F# 快速教程

文章目录

  • 基础应用
    • 绑定变量
    • 运算符
  • 数据类型
    • 基本类型
    • tuple, list, array, seq
    • map
    • set
    • record
    • DU
  • 函数
    • 函数复合
  • 面向对象
    • class
    • 抽象类和接口
  • 代码组织
    • 脚本
    • 编译F#文件
    • 在WPF中调用F#代码
    • 把F#转为C#

F#是微软开发的基于.NET平台的一款面向对象的函数式语言,安装过程非常简单,只需要下载最新版的VisualStudio,在安装的过程中选择F#即可。

创建项目也很简单,只要在VS中新建一个F#项目即可。快捷键F5启动程序,可以在命令行中查看输出。此外,微软还提供了fsi.exe这款命令行交互工具,可以通过dotnet fsi命令快速进入。

基础应用

绑定变量

在F#中,通过let关键字来定义变量、函数以及类等,接下来在fsi中演示这一功能。需要注意的是,命令行模式下的f#,需要以;;结尾来进行输出。

win+r输入cmd进入命令行,然后dotnet fsi开始交互操作。

>dotnet fsi

Microsoft(R) F# 交互窗口版本 F# 5.011.4.2.0
版权所有(C) Microsoft Corporation。保留所有权利。
若要获得帮助,请键入 #help;;

> let i = 1     //用//进行注释;let绑定变量时,会自动推断变量的数据类型
- ;;            //在命令行模式下,只有输入;;才会又输出
val i : int = 1

> let test = 
-   3*4+5*6;;   //let绑定变量时可以换行,但需要进行缩进
val test : int = 42

> let x,y,z = (1,2,3);; //可同时绑定多个变量,(1,2,3)为元组
val z : int = 3
val y : int = 2
val x : int = 1

> let result =
-     let i,j,k = (1,2,3)
-     i + 2*j + 3*k;;   //变量绑定时可以输入表达式
val result : int = 14

> let addOne a = a+1;;  //绑定函数
val addOne : a:int -> int

> addOne(1);;
val it : int = 2        //调用函数

运算符

运算符 类型 说明
&&、||、not 布尔 与、或、非
&&&,|||, ^^^,~~~
<<<、>>>
位运算 按位与、或、异或、取反
按位左移、右移
+、-、*、/、%、** 算术 加、减、乘、除、余、幂
=、<>、>=、<=、>、< 比较 字面意思

在F#中,运算符可以当作函数使用,而且在类中可以被重载,也可以定义一个全局的运算符。

> let a = (+) 1 2       //即a=1+2
- let op1 = (*) 3       //op1是一个函数,即op1(x) = 3*x
- op1(3);;              //调用op1并输出之前定义的这些东西
val a : int = 3
val op1 : (int -> int)
val it : int = 9

> let b = 4|> op1 |> op1;;  //4*3*3
val b : int = 36

> let (++)(x:int)(y:int) = x+2*y;;  //运算符重载
val ( ++ ) : x:int -> y:int -> int

> printf "%d"(10++1);;
12val it : unit = ()

其中|>位管道表达式,熟悉命令行的朋友应该清楚,4|> op1 |> op1表示4被op1作用一次再传给下一个op1,最后输出。

数据类型

基本类型

编程语言中的基本数据类型往往大同小异,F#中的基本数据类型基于.NET基元类型,更是如此。其中unit是独有的一种类型,表示空值,其只有一个取值()

类型 取值 类型 取值 类型 取值
bool True/False byte 0-255 sbyte -128~127
int16 16位有符号整型 uint16 16位无符号整型
int 32位有符号整型 uint32 32位无符号整型
int64 64位有符号整型 uint64 64位无符号整型
nativeint 有符号整型指针 unativeint 无符号整型指针
char Unicode字符 string 字符串
uint () void 无类型或无值
decimal 浮点 float32
single
32位浮点 float/double 64位浮点

tuple, list, array, seq

tuple

元组是一种常见的数据类型,在F#中,元组内部的元素可以是不同的数据类型甚至表达式。

> let tup1 = (1,'a',2.5)    //元组中可有不同类型元素
- let a,_,_ = tup1  //元组索引时,可以通过'_'来避免创建新的不需要的变量
- let third (_,_,c) = c     //定义一个函数,返回三元元组中的第三个值
- third(tup1);;             //执行函数,并输出
val tup1 : int * char * float = (1, 'a', 2.5)
val a : int = 1
val third : 'a * 'b * c:'c -> 'c
val it : float = 2.5

> let tup2 = (tup1,'a',third);; //元组中的元素甚至可以是函数
val tup2 : (int * char * float) * char * ('a * 'b * 'c -> 'c)
> let d = third tup2 tup1;;     //third(tup2)还是third,然后third再执行tup1
val d : float = 2.5

list

在列表中,元素必须类型相同,且值不可更改。F#中封装了List模块,提供了许多便利的函数。

> let L1 = [1;2;3]  //list用[]创建,内部元素用;隔开
- let L2 = []       //list可以为空
- let L3 = [        //list还可以通过换行来创建
-     1             //注意缩进
-     2
-     3];;
val L1 : int list = [1; 2; 3]
val L2 : a list
val L3 : int list = [1; 2; 3]

> let L4 = [1..4];; //创建从1到5的数列
val L4 : int list = [1; 2; 3; 4]

> let L5 = 0::L4;;   //用::来连接元素与列表
val L5 : int list = [0; 1; 2; 3; 4]

> let L6 = L5@L4;;  //用@来连接列表和列表
val L6 : int list = [0; 1; 2; 3; 4; 1; 2; 3; 4]

> let L7 = [for i in 1..5 -> i*i];; //序列表达式
val L7 : int list = [1; 4; 9; 16; 25]

> let L8=[
-     for i in 0..5 do
-         for j in 0..5 do
-             if (i+j)%2=1 then
-                 yield(i,j)];;
val L8 : (int * int) list =
  [(0, 1); (0, 3); (0, 5); (1, 0); (1, 2); (1, 4); (2, 1); (2, 3); (2, 5);
   (3, 0); (3, 2); (3, 4); (4, 1); (4, 3); (4, 5); (5, 0); (5, 2); (5, 4)]

//F#中封装了一些常用的List函数

> let sum1 = List.sum L1;;
val sum1 : int = 6

> let ave1 = List.averageBy (fun elem -> float elem) L1;;
val ave1 : float = 2.0
//List.average只能作用于浮点型的列表,所以List.average L1会报错
//List.averageBy的输入输出为函数,其后面的fun表达式表示将元素转为浮点型
let unzip8 = List.unzip L8;;    //将元组列表拆分
val unzip8 : int list * int list =
  ([0; 0; 0; 1; 1; 1; 2; 2; 2; 3; 3; 3; 4; 4; 4; 5; 5; 5],
   [1; 3; 5; 0; 2; 4; 1; 3; 5; 0; 2; 4; 1; 3; 5; 0; 2; 4])

array

和列表相比,数组内部的值是可变的。

> let arr1 = [|1;2;3;4;5|]      //通过[| |]定义,元素间用";"间隔
- let arr2 = [|"hello";"world";"and";"hello";"world";"again"|]
- let arr3 = [|for i in 1..5 -> i*i|];; //支持序列表达式
val arr1 : int [] = [|1; 2; 3; 4; 5|]
val arr2 : string [] = [|"hello"; "world"; "and"; "hello"; "world"; "again"|]
val arr3 : int [] = [|1; 4; 9; 16; 25|]

> let arr4 = Array.create 3 4;; //Array.create创建数组
val arr4 : int [] = [|4; 4; 4|]

> arr4.[2] <- 2;;       //通过.[]索引、通过<-赋值
> arr4;;
val it : unit = ()
val it : int [] = [|4; 4; 2|]

> let arr5 = Array.append arr1 arr3;;   //合并两个数组
val arr5 : int [] = [|1; 2; 3; 4; 5; 1; 4; 9; 16; 25|]

seq

序列主要用于索引,相当于python中的Range,但是其内部的数据类型可以为非整型。

>let seq1 = Seq.empty //空序列
-let seq2 = seq {
      yield "hello"; yield "world"; yield "and"; yield "hello"; yield "world"; yield "again" }
-let seq3 = seq {
      1 ..2 .. 10 }  //从1到10,间隔为2的序列[1;3;5;7;9]
>;;
val seq1 : seq<`a>
val seq2 : seq<string>
val seq3 : seq<int>

//L9选出seq2中包含'l'字母的单词
> let L9 = [
-     for word in seq2 do
-         if word.Contains("l") then
-             word];;
val L9 : string list = ["hello"; "world"; "hello"; "world"]

map

映射(字典)是一种由键值对组成的索引类型,与数组等相比,其键值可以为非整型

> let M1 = Map.empty;;  //通过Map.empty创建空字典
val M1 : Map<`a,`b> when `a : comparison

> let M2 = M1.Add("a",1).Add("b",2);;
val M2 : Map<string,int> = map [("a", 1); ("b", 2)]

> M2.["a"];;            //通过.[]索引键得到值
val it : int = 1

//通过Map.ofList可创建字典,list的元素为元组
> let M3 = Map.ofList ["a",1;"b",2;"c",3];;
val M3 : Map<string,int> = map [("a", 1); ("b", 2); ("c", 3)]

let seq4 = seq{
     for key in 1..5 do yield key,key.ToString()};;
val seq4 : seq<int * string>

> let M4 = Map.ofSeq seq4;;     //通过Map.ofSeq创建字典
val M4 : Map<int,string> =
  map [(1, "1"); (2, "2"); (3, "3"); (4, "4"); (5, "5")]

set

相对于序列来说,集合中没有重复元素。

> let set1 = Set.empty;;
val set1 : Set<'a> when 'a : comparison

//通过Add添加值
> let set2 = set1.Add("a").Add("b").Add("b");;
val set2 : Set<string> = set ["a"; "b"]     //set中没有重复元素

> let set3 = Set.ofList [1;2;3;4;1;3;5;2];;
val set3 : Set<int> = set [1; 2; 3; 4; 5]

record

record在形式上与字典类似,但与此前提到的数据类型不同,record需要首先通过关键字type进行预定义实际的类型,然后再通过实际类型的名称进行实例化,有点类或者结构的感觉。

//定义了一个名为fullName的类型
> type fullName = {
     First:string; Last:string;} ;;
type fullName =
  {
      First: string
    Last: string }

//定义值的时候不要显式声明
> let a = {
      First = "Tiny";Last = "Cool"};;
val a : fullName = {
      First = "Tiny"
                     Last = "Cool" }

> type petName = {
     First:string; Last:string;};;
type petName =
  {
      First: string
    Last: string }

//如果字段重复,可以通过":"来明确类型
> let b:petName = {
      First = "CS";Last = "DN"};;
val b : petName = {
      First = "CS"
                    Last = "DN" }
//复制并更新
> let c = {
     b with Last = "DNDNDN"};;
val c : petName = {
      First = "CS"
                    Last = "DNDNDN" }

> let printName(name: fullName)=
-     "my name is "+a.First+a.Last;;    //用.来索引
val printName : name:fullName -> string

> printName a;;
val it : string = "my name isTinyCool"

// 在Person中添加成员函数
> type Person =
-    {
      First: string
-      Last: string }
-
-      member this.printFirst() = this.First;;
type Person =
  {
      First: string
    Last: string }
  with
    member printFirst : unit -> string
  end
> d.printFirst();;
val it : string = "CS"

DU

即discriminated union,与record相似,DU也需要首先通过关键字type对具体的数据类型进行定义。其内部存储的是一组不相同的字段,在建立一个DU实例的时候,只能选取其中的一个字段,类似于枚举类型。

//联合的各个分量通过"|"来区分,字段首字母需要大写
> type person = |Man |Woman;;
type person =
  | Man
  | Woman

> let p1 = Man;;
val p1 : person = Man

> type personAge =
-     |Man of int       //联合也可以换行输入
-     |Woman of int;;   //通过of来规范字段的数据类型
type personAge =
  | Man of int
  | Woman of int

> let p2 = Woman(15);;      //一个15岁的女孩儿
val p2 : personAge = Woman 15

函数

所有编程语言都有函数,F#比较特殊的地方是递归函数需要用关键字rec声名。

在F#中,判断语句为if...then..elif...then...else,循环结构包括for...[to|downto] ...do ,for...in...do,while...do三种形式。其中,

  • for i = a to b语句类似于其他语言中常见的for(i=a;i<=b;i++)写法;downto则相当于i--
  • for i in b do即用i来遍历序列、列表或者数组b,相当于python中的for i in b:
  • while...do则是常见的表达形式.
    此外,F#还提供了类似于switch...case语句的match表达式,其表达式为match ...with...|...|...,其中|相当于case

作为函数式语言,F#理所当然地支持lambda表达式,其关键字为fun

> let f1 x = x+1.0;;
val f1 : x:float -> float

> let f2 (a,b) = a+b;;
val f2 : a:int * b:int -> int

> let f3(a,b) = (b,a);;
val f3 : a:'a * b:'b -> 'b * 'a

> let f4(a:int) = a+1;; //限定函数的输入数据类型
val f4 : a:int -> int

> let rec fac n =       //关键字rec定义递归函数
-     if n <= 1 then 1  //F#中无"=="号,"="用于比较,赋值用"<-"
-     else n*fac(n-1);; 
val fac : n:int -> int

> fac(5);;              //本函数实现阶乘
val it : int = 120

> let rec fib n =       //臭名昭著的斐波那契数列递归写法
-     match n with
-     | 0 | 1 -> n
-     | n -> fib(n-1)+fib(n-2);;
val fib : n:int -> int

函数复合

在F#中,函数可以通过运算符>>进行组合,表达式f1>>f2>>f3类似于f3(f2(f1(input)));也可以通过运算符|>进行管道处理,表达式x |> f1 |> f2 |> f3类似于f3(f2(f1(x)))

> let square x = x * x
- let addOne x = x + 1
- let isOdd x = x % 2 <> 0
- let numbers = [ 1; 2; 3; 4; 5 ];;
val square : x:int -> int
val addOne : x:int -> int
val isOdd : x:int -> bool
val numbers : int list = [1; 2; 3; 4; 5]


> let Com1 values =
-     let odds = List.filter isOdd values   //选出isOdd values为True时的元素
-     let squares = List.map square odds    //对odds中的元素根据square进行映射
-     let result = List.map addOne squares
-     result;;          //输出result
val Com1 : values:int list -> int list

> Com1([1;2;3;4;5]);;
val it : int list = [2; 10; 26]


//composition2的输出也为函数
let composition2 values = 
  List.map addOne (List.map square (List.filter isOdd values))

//通过|>运算符将函数进行组合
let pipe1 values =
        values
        |> List.filter isOdd
        |> List.map square
        |> List.map addOne

    //pipe
    let pipe2 values =
        values
        |> List.filter isOdd
        |> List.map(fun x -> x |> square |> addOne)	//通过lanmbda表达式和|>将square和addOne组合

    let composition3 =
        List.filter isOdd >> List.map (square >> addOne)

面向对象

所谓对象(object)就是封装了数据和算法的一个抽象模块,类似此前提到的record。面向对象在处理状态变化的事务时具有无可比拟的优势,尽管就开发来说,函数式有其在数据处理上的优势,但在某些情况下,面向对象还是不可或缺的。

class

在F#中,通过关键字type来定义类,其成员可以写在class...end的代码块中,当然这是可以省略的。

继承的关键字为inherit,多态虚拟成员abstract和重载overide实现。泛型类通过标识符<'A>来实现。需要注意的是,在F#中,类只能继承一个父类,但是可以继承多个接口。

//创建一个二维矢量
//F#会默认创建隐式字段dx和dy
> type Vector2D(dx:double, dy:double) = 
-     let length = sqrt(dx*dx + dy*dy)
-     member this.DX = dx   //this非关键字
-     member this.DY = dy
-     member this.Length = length
-     member this.Scale(k) = Vector2D(k*this.DX, k*this.DY);;
type Vector2D =
  new : dx:double * dy:double -> Vector2D
  member Scale : k:double -> Vector2D
  member DX : double
  member DY : double
  member Length : double

> let v1 = Vector2D(3.0, 4.0)
> let v2 = v1.Scale(10.0);;
> printfn "Length of v1: %f\nLength of v2 : %f"  v1.Length v2.Length;;
Length of v1: 5.000000
Length of v2 : 50.000000
val it : unit = ()

> type xAxis(x) =
-     inherit Vector2D(x,0.0)   //inherit表示继承
-     member this.Scale(k) = xAxis(k*x);;   //覆盖类成员  

当然抽象成员也是少不了的

> type extVector(dx:double,dy:double) =
-     let length = sqrt(dx*dx+dy*dy)
-     abstract member PrintInfo: unit->unit
-     default this.PrintInfo() = printfn "common vector";;
type extVector =
  new : dx:double * dy:double -> extVector
  override PrintInfo : unit -> unit + 1 重载

> type axisX(dx) =
-     inherit extVector(dx,0.0)
-     override this.PrintInfo() = printfn "x axis";;   //重载
type axisX =
  inherit extVector
  new : dx:double -> axisX
  override PrintInfo : unit -> unit

//状态跟踪
> type StateList<'T>(init: 'T) =
-     let mutable states = [ init ]
-     member this.insert newState =
-         states <- newState :: states  // 合并states
-     member this.History = states;;
type StateList<'T> =
  new : init:'T -> StateList<'T>
  member insert : newState:'T -> unit
  member History : 'T list

> let tracker = StateList 10;;  //int型的状态跟踪器
val tracker : StateList<int>

> tracker.insert 17;;           // 添加状态
val it : unit = ()
//可以通关过的形式进行实例化
> let strTracker = StateList<string> "First";;  
val strTracker : StateList<string>

抽象类和接口

在上例中,对于extVector这个类而言,如果虚拟函数PrintInfo后面没有定义默认方法是会报错的。

如果虚拟函数后面没有默认的实现代码,那么就会变成抽象函数,而包含了抽象函数的类则也就变成了抽象类。抽象类的定义前必须加上标记[AbstractClass]

如果一个类中,只有抽象成员,那么这个类又会摇身一变,而成为接口(Interface)。和类一样,接口也通过type来进行声明,并且写在interface...end代码块中。当然,又和类一样,interface,end也可以省略,编译器如果发现类中全都是抽象成员,那么会自动判定其为接口。

无论是抽象类还是接口,都不能创建实例,而只能被其他类所继承。

> type Speed =
-     abstract Run: dis :float -> float;;//只包含虚拟函数且没有初始化,自动判定为接口
type Speed =
  abstract member Run : dis:float -> float

> type Vehicle(speed:float)=    //通过接口实现了一个类
-     interface Speed with
-         member this.Run x = x/speed   //接口成员需要缩进
-     abstract member PrintInfo : unit -> unit
-     default this.PrintInfo() = printfn "speed:%f" speed;;
type Vehicle =
  interface Speed
  new : speed:float -> Vehicle
  override PrintInfo : unit -> unit + 1 重载

> let v = new Vehicle(15.0);;
val v : Vehicle
> let mutable ir = v :> Speed;; //接口子类可向上转换为接口类型的对象
val mutable ir : Speed

> printfn "10km with time:%.1f" (ir.Run(10.0));;
10km with time:0.7
val it : unit = ()

代码组织

脚本

F#中可以通过dotnet fsi test.fsx来调用脚本,例如新建一个

//test.fsx
let getOddSquares xs =
    xs
    |> List.filter (fun x -> x % 2 <> 0)
    |> List.map (fun x -> x * x)

printfn "%A" (getOddSquares [1..10])

然后

>dotnet fsi test.fsx
[1; 9; 25; 49; 81]

如果想在fsi中调用,可以

> #load "test.fsx";;
[正在加载 E:\Documents\00\1016\test.fsx]
[1; 9; 25; 49; 81]
namespace FSI_0002
  val getOddSquares : xs:int list -> int list
printfn $"%d{
       square 12}"

编译F#文件

若想编译F#文件,一般则需要定义入口函数。如果不定义,那么程序将顺序执行。在VS中新建一个F#文件,输入以下内容

let getOddSquares xs =
    xs
    |> List.filter (fun x -> x % 2 <> 0)
    |> List.map (fun x -> x * x)

printfn "%A" (getOddSquares [1..10])

然后用F5或者点击启动按钮,则得到其输出

[1; 9; 25; 49; 81]

若添加入口函数,即

let getOddSquares xs =
    xs
    |> List.filter (fun x -> x % 2 <> 0)
    |> List.map (fun x -> x * x)

printfn "%A" (getOddSquares [1..10])

[<EntryPoint>]    //进入点标志
let main argv =
    printfn "%A" (getOddSquares [1..20])
    0           // 返回一个整型数据

则在顺序执行程序之后,进入main函数,运行后得到结果如下。

[1; 9; 25; 49; 81]
[1; 9; 25; 49; 81; 121; 169; 225; 289; 361]

在WPF中调用F#代码

可能对于微软来说,用WPF做前端、C#做逻辑最后F#做计算才是一名dotneter的正常工作模式,所以在C#中调用F#代码就十分重要。

首先做一个F#的快速排序,一般快排需要随机选择一个中间值,那么下面的算法中,将数组的第一个值作为中间值。其中List.partion是通过某个规则来分割List;List.concat是合并List。可见F#可以通过4行代码实现快排,可以说很简洁了。

let rec qSort = function
  |[]->[]
  |head :: tail ->
    let L, R = List.partition ((>=) head) tail
    List.concat [qSort L; [head]; qSort R]

printfn "%A" (qSort [1;5;23;18;9;1;3])

*该写法转自这个教程。我自己写一般会用List.filter,不仅代码多了一倍,而且可能时间也会多一倍-_-||…

然后新建一个VS工程,在解决方案下分别新建一个WPF项目和一个由F#编写的.net类库,考虑到想学习F#的人大概率是C#老手,这里就不贴图了。

我建的WPF项目名称为TestCF,创建的F#类库命名为Sorts。接下来在TestCF的依赖项中添加引用项目,选中Sorts

然后就可以直接在MainWindow.xaml.cs中引用Sorts了。

using Sorts;

然后在Library.fs中新建一个module,并添加我们的快排算法

namespace Sorts
module Sort = 
    let rec qSort = function
    |[]->[]
    |head :: tail ->
        let L, R = List.partition ((>=) head) tail
        List.concat [qSort L; [head]; qSort R]

    let qSortC ra = //用于数据类型的转换
        ra |> Array.toList |> qSort |> List.toArray

然后重新生成解决方案,再编辑xaml.cs文件

<Window x:Class="TestCF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="250" Width="400">
    <StackPanel>
        <WrapPanel>
            <TextBlock Text="输入数组" Margin="5"/>
            <TextBox Width="200" Margin="5" x:Name="txtOriData"/>
            <Button Content="运行" Click="btnSort_Click" Margin="5"/>
        WrapPanel>
        <WrapPanel>
            <TextBlock Text="排序结果" Margin="5"/>
            <TextBox Width="200" Margin="5" x:Name="txtSort"/>
        WrapPanel>
    StackPanel>
Window>

下面是cs代码。

using System.Linq;
using System.Windows;
using Sorts;

namespace TestCF{
     
    public partial class MainWindow : Window{
     
        public MainWindow(){
     
            InitializeComponent();
        }

        private void btnSort_Click(object sender, RoutedEventArgs e){
     
            var oriData = txtOriData.Text.Split(' ')
                    .Select(i => int.Parse(i)).ToArray();
            txtSort.Text = string.Join(' ', Sort.qSortC(oriData));
        }
    }
}

结果为

F# 快速教程_第1张图片

把F#转为C#

比调用更极端的就是直接转化。由于无论是C#还是F#在.Net这个层面上都是想通的。先将F#转为.Net的中间语言,然后再将这个中间语言反编译成C#,就可以实现两种不同语言的转化了。

所以,开始之前需要下载一个反编译工具,这里推荐dnSpy,目前最新版本是6.1.8,免安装解压即用。

在F#的工程目录下,可以找到\bin\Debug\net5.0路径,里面有Sorts生成的dll文件,把这个文件用dnSyp打开就行,打开Sort文件夹下的qSortqSrotC,可以看到

//qSortC
public static a[] qSortC<a>(a[] ra)
{
     
	return ListModule.ToArray<a>(Sort.qSort<a>(ArrayModule.ToList<a>(ra)));
}
//qSort
public static FSharpList<a> qSort<a>(FSharpList<a> _arg1)
{
     
	if (_arg1.TailOrNull != null)
	{
     
		FSharpList<a> tail = _arg1.TailOrNull;
		a head = _arg1.HeadOrDefault;
		a x = head;
		Tuple<FSharpList<a>, FSharpList<a>> tuple = ListModule.Partition<a>(new Sort<a>.qSort@11(x), tail);
		FSharpList<a> R = tuple.Item2;
		FSharpList<a> L = tuple.Item1;
		return ListModule.Concat<a>(FSharpList<FSharpList<a>>.Cons(Sort.qSort<a>(L), FSharpList<FSharpList<a>>.Cons(FSharpList<a>.Cons(head, FSharpList<a>.Empty), FSharpList<FSharpList<a>>.Cons(Sort.qSort<a>(R), FSharpList<FSharpList<a>>.Empty))));
	}
	return FSharpList<a>.Empty;
}

你可能感兴趣的:(一文入门系列,.Net学习,F#,C#,.net,混合编程,教程)