学习笔记-涛讲F#(基础 II)
处理一堆数
一个处理集合数据的示例:
let a=[1..10] //List
let b=[| 1..10|] //Array
//for循环打印
for i in a do
printfn"%d,"i
//打印每个数
a |>Seq.iter(printfn"%d")
//将数转成函数
let makeF x=fun()->printfn"%d"x
a |>Seq.map makeF |>Seq.iter(fun f->f())
//将数转成函数再相加
let y=a |> Seq.map makeF |> Seq.reduce(>>)
y()
上面的示例需要注意几个知识点:
- for循环:与C#不同,F#的for循环表达式有 for...in 表达式(访问可枚举集合)、for...to 表达式(访问循环变量值-与C#类似)
- 数组:以 [| 和 |] 格式包围数据,处理数组的操作参考 Array 模块
- 列表:以 [ 和 ] 格式包围数据,对列表的基本操作参考 列表模块
- Seq:seq<'T> 类型(IEnumerable
的别名)表示序列, Seq 模块支持涉及序列的操作
以数组为例,创建数组的示例如下:
//用分号分隔来创建一个小型数组
let array1 = [| 1; 2; 3 |]
//每个元素各占一行,分号分隔符可选
let array1 =
[|
1
2
3
|]
//使用序列表达式来创建数组
let array3 = [| for i in 1 .. 10 -> i * i |]
//创建一个所有元素都初始化为零的数组
let arrayOfTenZeroes : int array = Array.zeroCreate 10
//在每个索引上调用给定的生成器来创建一个列表
let list = List.init 4 (fun x->0)
组织代码(命名空间、模块)
命名空间的定义参考 命名空间 (F#),注意以下几点:
- 如果要将代码放在命名空间中,文件中的第一个声明必须声明该命名空间, 整个文件的内容将成为命名空间的一部分。
- 如果文件中存在其他命名空间声明,则直到下一个命名空间声明之前的所有代码都被认为在第一个命名空间内。
- 命名空间不能直接包含值和函数,值和函数必须包含在模块中, 命名空间可以包含类型和模块。
模块的定义参考 模块,注意以下几点:
- F# 模块是一组 F# 代码构造,例如类型、值、函数值和 do 绑定中的代码。
- 模块声明有两种类型,具体取决于整个文件是否包含在模块中:顶级模块声明和局部模块声明, 顶级模块声明在模块中包括整个文件。
实验记录:将一个Namespace下的若干Module编译为dll后使用C#调用,可以看到在C#中,module被视作class,而module中的函数被视作class的公共静态Method,变量被视作class的静态只读Property。
很多人确实这么做了,可以帮助理解,对于混合编程也有好处。等你习惯了以后,尽量使用脱离C#的想法来思考F#。就象学外语一样,老翻译成中文学,刚开始可以,习惯以后还是要用外语思考的好。通常来说,F#编译器产生的代码和C#产生的是不一样的,当IL被转成一个C#以后,会造成错觉的。
新建一个 Module 文件,新建的文件会在已有文件的前面且不能调换顺序:
Module.fs文件内容:
namespace MyNamespace
//命名空间不能定义函数
//let f3()=()
type stringAlias=String
module Module1=
let printInt =printfn "%d"
module Module2=
let f()=printfn "in function f"
Program.fs文件内容:
open MyNamespace
open Module1
let a=[1..10]//List
let b=[| 1..10|]//Array
for i in a do
printfn "%d,"i
MyNamespace. Module2.f()
a |>Seq.iter printInt
使用联合重命名类型
使用联合重命名类型有点类似于C++的typedef关键字,但F#中多了类型推断程序更不容易出现bug。F#编译器做了很多优化,不能担心联合重命名类型时的性能。
type HumanName = |HumanName of string
type DogName = | DogName of string
//推断输入参数类型
let f=function
|HumanName(n)->printfn "%s"n
let dogName=DogName("lucky");
let name=HumanName("Tao")
f name
类必须显式转换成接口
接口方法只能通过接口调用,不能通过实现接口的类型的任何对象调用。 需要使用 :> 运算符或 upcast 运算符向上转换到接口类型,才能调用这些方法,如以下代码所示:
//接口定义
type IAsType<'T>=
abstract member NewValue:'T with get, set
type MyClass()=
let mutable v=8
interface IAsType<int>with
member this.NewValue
with get()=v
and set(i)=v<-i
interface IAsType<string>with
member this.NewValue
with get()=v.ToString()
and set(i)=v <- System. Convert.ToInt32(i)
let mc=MyClass()
//显示转换
let a=mc:>IAsType<int>
let b=mc:>IAsType<string>
a.NewValue <- 4
b.NewValue
注:在泛型语法中的type-parameters 是一个表示未知类型的参数的逗号分隔列表,其中每个参数都以单引号开头,并且可选择包含一个约束子句,用来进一步限制可用于该类型参数的类型。
对象表达式
对象表达式是介于module和class之间的代码组织方式,可用于创建动态创建的匿名对象类型的新实例,该对象类型基于现有基类型、接口或接口集。
//object expression
type IA=
abstract MyFunction: int->int
abstract MyValue: string with get, set
let myFunction i=i+1
let mutable str ="Hello"
let a=
{
new IA with
member this. MyFunction i=myFunction i
member this. MyValue
with get()=str
and set(i)=str<-i
}
递归函数
递归函数:rec 关键字与 let 关键字一起用于定义递归函数,参考 递归函数:rec 关键字 。
一个简单的递归函数示例:
let l=[1L..100L]
let rec sumBad l=
match l with
|[]->0L
|h::t->h+(sumBad t)
let ybad=sumBad l
printfn "%A"ybad
CPS解决堆栈溢出
上面的递归示例的原始数据如果变成 let l=[1L..1000000L] 则会产生堆栈溢出,使用CPS可以避免堆栈溢出。
CPS是函数编程语言里面常识,有两种翻译:
- continuous passing style(CPS)
- continuous programming style(CPS)
使用CPS必须在“解决方案”->“属性”->"生成"里面勾选“生成尾调用”,改造后递归函数示例:
let l=[1L..1000000L]
//cont是continue的缩写,表示递归的结束函数
let rec sum l cont=
match l with
|[]->cont 0L
|h::t->
sum t (fun x->cont(h+x))
//id函数是Operators模块里面的函数,会返回输入的参数值
let y=sum l id
printfn "%A"y
有两个递归调用的CPS:
//List.filter类似于LINQ的where关键字
let rec qs list cont =
match list with
|[]->cont([])
|[a]->cont([a])
| head:: tail->
let lessList=tail |>List.filter(fun i->i<=head)
let moreList=tail |>List.filter(fun i->i>head)
qs lessList (fun lessListPara -> //lesslistPara=gs lesslist
qs moreList (fun moreListPara->cont(lessListPara@[head]@ moreListPara)))
let list=[0;7;2;6;8;4;1;12]
let result=qs list id
for i in result do
printfn"%A"i
上面的函数作用是排序列表,先取头元素然后筛选出比头元素小的左列表、比头元素大的右列表,分别递归排序完左列表、右列表,最后按 左列表+头元素+右列表 组合出结果列表。
扩展一个类型
类似于C#中的扩展方法,参考 类型扩展,示例代码如下:
type Variant=
| HugeNumber of int
| BigNumber
| SmallNumber
module FunctionLibrary=
// function 用作 fun 关键字和 Lambda 表达式中对单个参数进行模式匹配的 match 表达式的较短替代项
// let print = function 等价于 let print x = match x with
let print=function
| HugeNumber n->printfn "Num %d"n
| BigNumber->printfn $"{nameof(BigNumber)}"
| SmallNumber->printfn "Small number"
//为Variant 添加扩展
type Variant with
//x只是自标识符,可以任意取名,如this、self
member x.Print()=FunctionLibrary. print x
let a=Variant.SmallNumber
let b=Variant.HugeNumber 100
//个人感觉使用管道符调用更好看一点,符合函数思想
a |>FunctionLibrary. print
b.Print()
静态解析的类型参数
静态解析的类型参数是一种类型参数,它在编译时(而不是在运行时)被替换为实际类型。 它们前面有一个插入符号 (^),参考 静态解析的类型参数。
在 F# 中,有两种不同的类型参数:
- 标准泛型类型参数:参数由撇号 (') 表示,如 'T 和 'U,等同于其他 .NET Framework 语言中的泛型类型参数。
- 静态解析的参数:用一个插入符号表示,如 ^T 和 ^U。
静态解析的类型参数示例如下:
type Adder()=
member this. Add(a,b)=a-b
let inline add (obj:^T) a b=(^T:(member Add: int->int->int)(obj,a,b))
let adder=Adder()
printfn"%d"(add adder 2 3)
上面的add函数类似一种反射调用,前面的类型约束相当于是对实例方法的filter(类似GetMethod),后面的括号就是对反射获取的方法进行调用(类似Invoke),第一个参数是实例,后面则是参数列表。
注:定义类时,成员函数的参数括号不是代表元组,只是为了与C#保持语法上面的一致,简单来说示例中的Add(a,b)等价于Add a b。
ref变量的实现原理及应用
ref变量本质上是通过一个非常简单的记录来实现的,该记录包含一个可变记录字段,一个简单的示例:
//定义
let a=ref 1
//赋值
a:=3
//通过Value赋值
a.Value<-5
//读取
printfn "%d"!a
//通过Value读取
printfn "%d"a.Value
//引用同一个记录
let b = a
b := 10
printfn "%d"!a
参考 F#: let mutable vs. ref,ref的实现原理如下:
type ref<'T> = // '
{ mutable value : 'T } // '
// the ref function, ! and := operators look like this:
let (!) (a:ref<_>) = a.value
let (:=) (a:ref<_>) v = a.value <- v
let ref v = { value = v }
F#资源网站
F#学习、工作中的一些资源网站:
- B站 涛讲F# :设计和开发F#语言作者之一,本学习笔记的视频来源。
- F#文档:微软F#文档,可以查询F#语法的基础定义和示例。
- F#核心库文档:微软放在github上的F#核心库文档。
- F#片段:一些好用的F#代码片段,可以学习其中的用法。
- Sergey Tihon's Blog:每周发布一些F#的新闻资讯。
- F# for Fun and Profit:里面的内容完全可以让新手入门、熟手进阶。
- Microsoft.FSharp.Collections 命名空间 (F#):此命名空间包含一些非常适合在 F# 中使用的面向对象风格的常见集合,这个应该会经常用到。
Result数据类型
Result<'T,'TFailure>类型允许您编写可组合的容错代码,参考 Result模块。
结果类型是struct 可区分的 union,结构相等语义适用于此。该类型通常用于一元错误处理,在 F# 社区Result中通常称为面向铁路的编程。
下面的示例演示了Result的用法和一些简单数据类型转换:
//一元二次方程有没有实根
let f (a,b,c)=
let delta=b**2.-4.*a*c
if(delta<0.0) then Error "没实根"
else Ok delta
//数据类型的转换方法
let x=f(1.,float(2),4 |>float)
注:其它类型转换方法参考 强制转换和转换 (F#)。
面向铁路的编程与if...else类似,函数内部对Result做模式匹配:
let process0=function
|Ok x->
if x > 0 then
//单行注释
(* 多行注释*)
printfn "valid value, continue"
Ok(System.Random().Next(0,3))
else Error "Must be positive"
//使用as对Error变量重命名
| Error _ as y->y
//铁道模式
(Ok 1)|>process0 |>process0 |>process0 |>ignore
异常处理
F# 中有两种异常类别:.NET 异常类型和 F# 异常类型,通常不提倡在F#中使用异常,参考 异常类型。
常量 Literal
F#中有const关键字,但是作为关键字保留,以供将来扩充 F#。
可以使用 Literal 属性标记旨在成为常量的值, 此属性具有导致将值编译为常量的效果:
[<Literal>]
let π=3.1415926
AutoOpen 属性
如果要在引用某个程序集时自动打开命名空间或模块,可以将 AutoOpen 属性应用于该程序集。 还可以将 AutoOpen 属性应用于某模块,以在打开父模块或命名空间时自动打开该模块,参考 AutoOpenAttribute:
[<AutoOpen>]
module MyModule=
let a=100
//不使用AutoOpen只能提供MyModule.a访问
printfn "%d" a