F#语言快速教程

F#是微软开发的基于.NET平台的一种函数式语言,而且支持面向对象。
其安装过程非常简单,只需要下载最新版的VisualStudio,在安装的过程中选择F#即可。
创建项目也很简单,只要在VS中新建一个F#项目即可。快捷键F5启动程序,可以在命令行中查看输出。

文章目录

  • 基础
    • 绑定变量
    • 运算符
  • 数据类型
    • 基本类型
    • tuple
    • list
    • array
    • seq
    • map
    • set
    • record
    • DU
    • option
  • 函数
    • 函数复合
  • 面向对象
    • class
    • 抽象类和接口
  • 交互操作
    • fsi
    • 使用CLI

基础

绑定变量

在F#中,通过let关键字来定义变量、函数以及类等。
如果希望建立一个代码块,可以使用关键字module,从而避免创建全局变量

module keyLet=
    let i = 1           //F#中可以用可用//进行注释;let绑定变量时,会自动推断变量的数据类型
    let testValue = 
        3*4+5*6         //let绑定变量时可以换行,但需要进行缩进
    let x,y,z = (1,2,3) //可同时绑定多个变量,其中(1,2,3)为元组
    let result=         //body-expression
        let i,j,k = (1,2,3)
        i + 2*j + 3*k
    let function1 a=
        a+1             //let绑定函数

运算符

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

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

module opTest=
    let a = (+) 1 2     //即a=1+2
    let op1 = (*) 3     //op1是一个函数,即op1(x) = 3*x
    let b = 4 |> op1 |> op1 //  b = 3*3*4
    //通过let新建运算符
    let (++) (x: int) (y: int) = x + 2*y
    printf "%d" (10 ++ 1)

数据类型

基本类型

编程语言中的基本数据类型往往大同小异,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

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

module tupleTest=
    let tuple1 = (1,'a',2.5)        //元组中的元素可以是不同的数据类型
    let a,_,_ = tuple1              //元组索引时,可以通过'_'来避免创建新的不需要的变量

    let third (_,_,c) = c           //定义一个函数,返回三元元组中的第三个值
    let tuple2 = (tuple1,'a',third) //元组中的元素可以是元组和函数
    let d = third tuple2 tuple1     
    printfn "%f" d                  //输出为2.5

list

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

module listTest =
    let list1 = [1;2;3]             //list用[]创建,内部元素用;隔开
    let list2 = []                  //list可以为空
    let list3 = [                   //list还可以通过换行的方式创建
        1                           //此时可省略;
        2
        3]
    let list4 = [ 1 .. 5 ]          //创建从1到5的数列其值为
    printfn "%A" list4              //输出为[1;2;3;4;5]
    let list5 = 0::list4            //用::来连接元素与列表,得到[0;1;2;3;4;5]
    let list6 = list5@list4         //用@来连接列表
    let list7 = [for i in 1..5 -> i*i]  //序列表达式,得到[1;4;9;16;25]
    let list8 = 
        [ for i in 0 .. 7 do
              for j in 0 .. 7 do 
                  if (i+j) % 2 = 1 then 
                      yield (i, j) ]
    printfn "%A" list8
    (*输出为:
    [(0, 1); (0, 3); (0, 5); (0, 7); (1, 0); (1, 2); (1, 4); (1, 6); (2, 1); (2, 3);
    (2, 5); (2, 7); (3, 0); (3, 2); (3, 4); (3, 6); (4, 1); (4, 3); (4, 5); (4, 7);
    (5, 0); (5, 2); (5, 4); (5, 6); (6, 1); (6, 3); (6, 5); (6, 7); (7, 0); (7, 2);
    (7, 4); (7, 6)]*)
        ///常用的List函数
    let sum1 = List.sum list1   //List.sum 表示对列表求和
    let ave1 = List.averageBy (fun elem -> float elem) list1
    (*List.average只能作用于浮点型的列表,
    所以List.average list1会报错
    List.averageBy可以以一个函数表达式作为输入
    其后面的fun表达式表示将元素转为浮点型*)
    let unzip8  = List.unzip list8      //List.unzip可将元组列表拆分
    printfn "%A" unzip8
    (*s输出为:
    ([0; 0; 0; 0; 1; 1; 1; 1; 2; 2; 2; 2; 3; 3; 3; 3; 
      4; 4;4; 4; 5; 5; 5; 5; 6; 6;6; 6; 7; 7; 7; 7],
     [1; 3; 5; 7; 0; 2; 4; 6; 1; 3; 5; 7; 0; 2; 4; 6; 
      1; 3; 5; 7; 0; 2; 4; 6; 1; 3;5; 7; 0; 2; 4; 6])*)

array

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

module arrayTest =
    let arr1 = [|1;2;3;4;5|]                //数组通过[| |]进行定义,元素通过";"间隔,且可以为空
    let arr2 = [| "hello"; "world"; "and"; "hello"; "world"; "again" |]
    let arr3 = [| for i in 1..5 -> i*i |]   //支持序列表达式
    let arr4 = Array.create 3 5             //Array.create可创建数组,其值为[|5;5;5|] 
    arr4.[1] = 1                            //通过.[]进行索引,索引从0开始,arr4变为[|5;1;5|]
    arr4.[2] <- 2                           //通过<-进行赋值
    let arr5 = Array.append arr1 arr3      //用Array.append合并两个数组,List也支持append

seq

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

module seqTest = 
    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]
    //seq选出seq2中包含'l'字母的单词
    let seq4 = 
        seq {
      for word in seq2 do               //for..in..do表示遍历seq2中的元素
                  if word.Contains("l") then    
                      yield word }          //关键字yield可以将值插入到序列中

map

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

module mapTest=
    let map1 = Map.empty                    //通过Map.empty创建空的字典       
    let map2 = map1.Add("a",1).Add("b",2)   //.Add添加键值对
    printfn "%d" map2.["a"]                 //通过.[]索引键得到值,输出为1
    //通过Map.ofList可创建字典,list的元素为元组
    let map3 = Map.ofList ["a",1; "b",2; "c",3]
    let seq4 = seq{
     for key in 1..5 do yield key,key.ToString()}
    let map4 = Map.ofSeq seq4               //通过Map.ofSeq创建字典

set

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

module setTest=
    let set1 = Set.empty                    //通过Set.empty创建空的字典       
    let set2 = set1.Add("a").Add("b")       //.Add添加键值对
    let set3 = set2.Add("c").Add("b")   
    printfn "%A" set3                       //输出为["a";"b";"c"]
    let set4 = Set.ofList [1;2;3;4;1;3;5;2] //值为[1;2;3;4;5]

record

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

module recordTest=
    type fullName = {
     First:string; Last:string;}    //在实例化记录之前需要进行声明,分号可用换行代替
    let a = {
      First = "Wang";Last = "Xing"}        //定义值的时候不要显式声明

    type petName = {
     First:string; Last:string;}
    let b:petName = {
      First = "Wang";Last = "Xing"}//字段重复,可以通过":"来明确类型
    
    //"copy and update":c = { First = "Wang";Last = "Xing"}
    let c = {
     b with Last = "Xingxing"}

    let printName (name:petName)=
        "its name is"+c.First+c.Last                //记录通过.字段进行索引
    printfn "%s" (printName c)

    //记录中可以封装函数
    type autoFullName = 
        {
     First: string;
         Last: string;}
        member this.printFirst=
            "its first name is "+this.First
    
    let d:autoFullName = {
      First = "Wang";Last = "Xing"}
    printfn "%s" d.printFirst

DU

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

module duTest=
    //联合的各个分量通过"|"来区分,字段首字母需要大写
    type person = | Man |  Woman 
    let p1 = Man
    type personAge =        //联合也可以换行输入
        | Man of int            //通过of来规范字段的数据类型
        | Woman of int
    let p2 = Man(15)            //一个15岁的男人

option

option类型提供了一种自检机制,即可以自行判断数据中是否存在符合我们需求的值。其内部有三个字段,Value为访问基础类型值;isSome判断取值是否为基础类型值;isNone判断取值是否为None,即如果不确定某值是否有效,可先行通过isNone进行判断。

module optionTest=
    let mutable x = Some(50)    //可选类型通过Some定义
    x <- Some(40)

函数

所有编程语言都有函数,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

module functionTest=
    let func1 x = x+1.0
    let func2 (a,b) = a+b
    let func3 (a,b) = (b,a)
    let func4 (a:int) = a+1     //限定函数的输入数据类型
    printfn "%d" (func4 1)

    let rec fac n =             //关键字rec定义递归函数
        if n=0 then 1           //F#中无"=="号,"="只用于比较,赋值用"<-"
        else fac n*fac(n-1)     //本函数实现阶乘
    
    let rec fib n =             //臭名昭著的斐波那契数列递归写法
        if n<=2 then 1
        else fib (n-1)+fib(n-2)

    let rec printList listx =
        match listx with
        //如果可以拆分成一个值和一个list的和,则打印这个值并递归剩下的list
        | head :: tail -> printf "%d " head; printList tail
        //如果list为空,则打印""
        | [] -> printfn ""

    //lambda表达式,fun关键字
    let func5 list = List.map(fun i-> i+1)list
    printfn "%A" (func5([1;2;3;4;5]))

函数复合

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

module pipeComTest =

    let square x = x * x
    let addOne x = x + 1
    let isOdd x = x % 2 <> 0
    let numbers = [ 1; 2; 3; 4; 5 ]

    let composition1 values = 
        let odds = List.filter isOdd values         //List.filter 选出isOdd values为True时的元素
        let squares = List.map square odds          //List.map 对odds中的元素根据square进行映射
        let result = List.map addOne squares
        result
    
    //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#中,类只能继承一个父类,但是可以继承多个接口。

module classTest = 
    //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)   //方法,返回一个缩放后的向量
    
    let vector1 = Vector2D(3.0, 4.0)        //实例化
    let vector2 = vector1.Scale(10.0)       //调用scale方法
    printfn "Length of vector1: %f\nLength of vector2: %f" vector1.Length vector2.Length

    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 axisX(dx) = 
        inherit extVector(dx,0.0)
        override this.PrintInfo() = printfn "x axis"     //虚拟函数的重载

    type axisY(dy) = 
        inherit extVector(0.0,dy)
        override this.PrintInfo() = printfn "y axis"     //虚拟函数的重载

    type StateList<'T>(init: 'T) = 
        let mutable states = [ init ]
        member this.insert newState = 
            states <- newState :: states    // 合并newState和tstates并赋值给states
        member this.History = states        // Get the entire list of historical states
        
    let tracker = StateList 10                  //int型的状态跟踪器
    tracker.insert 17                           // 添加状态
    let strTracker = StateList<string> "First"  //可以通关过的形式进行实例化

抽象类和接口

在上例中,对于extVector这个类而言,如果虚拟函数PrintInfo后面没有定义默认方法是会报错的。
如果虚拟函数后面没有默认的实现代码,那么就会变成抽象函数,而包含了抽象函数的类则也就变成了抽象类。抽象类的定义前必须加上标记[AbstractClass]
如果一个类中,只有抽象成员,那么这个类又会摇身一变,而成为接口(Interface)。和类一样,接口也通过type来进行声明,并且写在interface...end代码块中。当然,又和类一样,interface,end也可以省略,编译器如果发现类中全都是抽象成员,那么会自动判定其为接口。
无论是抽象类还是接口,都不能创建实例,而只能被其他类所继承。

module interfaceTest=

    type Speed = 
        abstract 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

    let v1 = new Vehicle(15.0)
    let mutable ir1 = v1 :> Speed            //接口的子类可以向上转换为接口类型的对象
    printfn "跑10公里用时%.1f" (ir1.Run(10.0))

交互操作

fsi

在Visual Studio的安装目录中提供了一个交互式操作工具fsi.exe,其路径为
..\2019\Common7\IDE\CommonExtensions\Microsoft\FSharp\fsi.exe
这样我们就可以像使用python一样使用F#,可以非常方便地进行函数的调试。其界面如下:
F#语言快速教程_第1张图片

需要注意的一点是,在fsi中输入命令,如以双分号;;结尾,则可直接输出结果,有点像matlab不加分号的样子。

> let a = 1;;			//绑定a值为1
val a : int = 1

> let addOne x = x+1	//没有;;则不输出
- let addThree x = x|> addOne |> addOne |> addOne;;//管道操作
val addOne : x:int -> int
val addThree : x:int -> int

> printfn "%d" (addOne a);;
2
val it : unit = ()

> printfn "%d" (addThree a)
> ;;
4
val it : unit = ()

使用CLI

所谓CLI即command-line interface,也就是命令行的意思。.net在命令行上提供了dotnet工具,用以开发 .NET 应用程序。
只要安装.NET Core SDK即可启动dotnet工具了。而且对于F#,微软提供了一个简短的教程:使用F#命令行。

dotnet new sln -o test							//使用new命令新建一个项目
dotnet new classlib -lang F# -o src\Library		//创建一个由F#语言编写的classlib
dotnet sln test add src\Library\Library.fsproj	//将Library添加到test中的sln中

此时的目录结构为

E:\Documents\00工作文档\0713>tree /a /f
卷 工作 的文件夹 PATH 列表
卷序列号为 3895-5CDD
E:.
+---src
|   \---Library
|       |   Library.fs
|       |   Library.fsproj
\---test
        test.sln

其中,Library.fs为F#程序的源代码,打开之后发现默认定义了一个打招呼的函数:

namespace Library
module Say =
    let hello name =
        printfn "Hello %s" name

我们先build然后新建一个程序调用这个Library中的Say

dotnet build
dotnet new console -lang F# -o src\Hello	//用new命令新建一个F#语言的控制台程序
vim src\Hello\Program.fs					//编辑Program.fs文件

此时会发现文件夹src中多了一个Hello文件夹,编辑其中的Program.fs文件

open System
open Library	//调用自建的Library

[<EntryPoint>]
let main argv =
    Say.hello "World"

然后回到命令行运行这个程序

E:\Documents\00工作文档\0713\>cd src\Hello
E:\Documents\00工作文档\0713\src\Hello>dotnet run
Hello World			//输出结果

在F#中,主要通过模块和命名空间来对代码进行组织,而后编译成可执行文件exe或者动态链接库dll。通过dotnet new help命令可以查看我们可以创建的文件类别及其短名称。其中,console为Console Application的缩写,表示命令行应用;classlib为Class Library模板的缩写,为类库。

你可能感兴趣的:(一文入门系列,F#,.Net,面向对象,函数式,微软技术)