创建项目也很简单,只要在VS中新建一个F#项目即可。快捷键F5启动程序,可以在命令行中查看输出。此外,微软还提供了fsi.exe
这款命令行交互工具,可以通过dotnet fsi
命令快速进入。
在F#中,通过let
关键字来定义变量、函数以及类等,接下来在fsi
中演示这一功能。需要注意的是,命令行模式下的f#
,需要以;;
结尾来进行输出。
win
+r
输入cmd
进入命令行,然后dotnet fsi
开始交互操作。
>dotnet fsi
Microsoft(R) F# 交互窗口版本 F# 5.0 的 11.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
元组是一种常见的数据类型,在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"]
映射(字典)是一种由键值对组成的索引类型,与数组等相比,其键值可以为非整型
> 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")]
相对于序列来说,集合中没有重复元素。
> 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需要首先通过关键字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"
即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
则是常见的表达形式.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。面向对象在处理状态变化的事务时具有无可比拟的优势,尽管就开发来说,函数式有其在数据处理上的优势,但在某些情况下,面向对象还是不可或缺的。
在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#文件,一般则需要定义入口函数。如果不定义,那么程序将顺序执行。在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做前端、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));
}
}
}
结果为
比调用更极端的就是直接转化。由于无论是C#还是F#在.Net
这个层面上都是想通的。先将F#转为.Net
的中间语言,然后再将这个中间语言反编译成C#,就可以实现两种不同语言的转化了。
所以,开始之前需要下载一个反编译工具,这里推荐dnSpy,目前最新版本是6.1.8,免安装解压即用。
在F#的工程目录下,可以找到\bin\Debug\net5.0
路径,里面有Sorts
生成的dll
文件,把这个文件用dnSyp
打开就行,打开Sort文件夹下的qSort
和qSrotC
,可以看到
//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;
}