F#是微软开发的基于.NET平台的一种函数式语言,而且支持面向对象。
其安装过程非常简单,只需要下载最新版的VisualStudio,在安装的过程中选择F#即可。
创建项目也很简单,只要在VS中新建一个F#项目即可。快捷键F5启动程序,可以在命令行中查看输出。
在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位浮点 |
元组是一种常见的数据类型,在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
在列表中,元素必须是相同类型的,且值不可更改。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])*)
和列表相比,数组内部的值是可变的。
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
序列主要用于索引,相当于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可以将值插入到序列中
映射(字典)是一种由键值对组成的索引类型,与数组等相比,其键值可以为非整型
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创建字典
相对于序列来说,集合中没有重复元素。
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需要首先通过关键字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
即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类型提供了一种自检机制,即可以自行判断数据中是否存在符合我们需求的值。其内部有三个字段,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
则是常见的表达形式.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也可以作为一种对象。面向对象在处理状态变化的事务时具有无可比拟的优势,所以就开发的角度来说,函数式有其在数据处理上的优势,但在某些情况下,面向对象还是不可或缺的。
在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))
在Visual Studio的安装目录中提供了一个交互式操作工具fsi.exe,其路径为
..\2019\Common7\IDE\CommonExtensions\Microsoft\FSharp\fsi.exe
这样我们就可以像使用python一样使用F#,可以非常方便地进行函数的调试。其界面如下:
需要注意的一点是,在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即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模板的缩写,为类库。