函数式编程之-F#类型系统
目录
Tuple type
Record type
使用Record type来建立领域模型
Descriminated Unions type
使用 Descriminated Unions type来建立领域模型
在深入到函数式编程思想之前,了解函数式独有的类型是非常有必要的。函数式类型跟OO语言中的数据结构截然不同,这也导致使用函数式编程语言来解决问题的思路跟OO的思路有明显的区别。
什么是类型?类型在编程语言中有什么作用呢?一般来说,类型有两个作用:
- 首先当你对某个数据声明类型后,就拥有了编译时的检查,换句话说,你可以认为类型充当了“编译时的单元测试”;
- 类型系统可以让你建立一种模型,用来表达真实世界中的模型;
Tuple type
元组是函数式编程语言中的常用类型,同时在.NET 4.0中被引入到了C#中。但是在C#中几乎不太会用到这种类型,但是在函数式编程语言中却随处可见。
例如在C#中这样使用元组:
1 2 | var s = new Tuple< int , int >(1, 2); var fist = s.Item1; |
在F#中
1 2 3 4 5 | let t = 1,2 let first = fst t //提取第一个元素 let second =snd t //提取第二个元素 let f s = t //通过解构提取元素 |
Record type
Record type是F#中最常用的类型。经常被用来对领域建模(Domain modeling)。例如定义一个矩形数据结构:
1 2 3 4 5 6 | type Rect = { Left: float32 Top: float32 Width: float32 Height: float32 } |
这看起来跟OO语言中对class的定义是很相似的,好比在某个class中声明了一组属性。使用起来也简单:
1 2 3 4 5 6 | let rc = { Left = 10.0f; Top = 10.0f; Width = 200.0f; Height = 200.0f } |
你可以把它当做一个简单的class,但是从定义和使用都能看出来Record type更加简单和直接一些。并且我们并没有在Record type里设计一些方法,这跟class有本质的区别。
Record type还支持“复制一个现有记录并进行一些修改”:
1 | let rc2 ={ rc with Left = rc.Left + 100.0f } |
C#中的class是没有这种能力的,你不得不显示复制所有属性。
另外Record type自动实现了equal操作符:
1 2 3 4 5 | type Name = { First: string ; Last: string } let jim = { First = "Jim" ; Last = "Dan" } let jim2 = {First = "Jim" ; Last = "Dan" } let isSame = jim = jim2 //true |
使用Record type来建立领域模型
考虑下面的Contact领域模型:
1 2 3 4 5 6 7 8 9 10 11 | type Contact = { FirstName: string ; MiddleName: string ; LastName: string ; EmailAddress: string ; Address1: string ; Address2: string ; City: string ; State: string ; Zip: string ; } |
如果你把它当做一个class也是可行的,实际上在OO语言里我们也经常设计这样的class。这样的模型定义犯了三个错误:
- 没有把相关一组类型组合起来,例如FirstName, MiddleName, LastName。这三个类型共同组成了Name,我们的模型并没有体现出这样的设计。
- EmailAddress真的是一个string吗?他能有效的表达Email这样的Domain吗?Email分有效和无效,他拥有自己的规则,并不是所有的字符串都是Email,string这样的类型无法表达Email的Domain含义。
- 在F#中没有null,如果你认为某个类型可能为空,就应该设计为option类型,例如MiddleName,他应该是string option。
还记得前面的章节我们说函数式编程的核心思想是组合
,组合不但体现在函数之间的组合,类型也是可组合的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | type PersonalName = { FirstName: string ; MiddleName: string option; LastName: string ; } type EmailContactInfo = { EmailAddress: string ; IsEmailVerified: bool ; } type PostalAddress = { Address1: string ; Address2: string ; City: string ; State: string ; Zip: string ; } type PostalContactInfo = { Address: PostalAddress; IsAddressValid: bool ; } type Contact = { Name: PersonalName; EmailContactInfo: EmailContactInfo; PostalContactInfo: PostalContactInfo; } |
Descriminated Unions type
中文翻译过来叫做可区分联合
,这种类型试图为不同的选项进行建模,所以你可以把他理解为选项类型
。
举个例子:“现在温度是多少?“
如何对现在的温度建模?你问的是摄氏度呢还是华氏度呢?如果是摄氏度即38°,如果单位是华氏度,则为100.4°。
1 2 3 4 5 6 | type Temperature = | F of float | C of int let tempNow = C(30) let tempNow2 = F(100.4) |
只有一个选项的类型:
1 2 3 | type EmailAddress = EmailAddress of string let email = "a" |> EmailAddress let emails = [ "a" ; "b" ; "c" ] |> List.map EmailAddress |
使用 Descriminated Unions type来建立领域模型
选项类型
在F#是非常常用的领域模型建模类型,比如设计一个关于支付的模型,在OO语言中,你可能会这样做:
1 2 3 4 5 6 7 8 | interface IPaymentMethod { } class Cash : IPaymentMethod { } class Cheque: IPaymentMethod { } class Card : IPaymentMethod { } ... |
在函数式语言中利用选项类型
可以轻松搞定:
1 2 3 4 | type PaymentMethod = | Cash | Cheque of ChequeNumber | Card of CardType * CardNumber |
OO思想中通过抽象接口和定义派生类来实现这个模型。函数式语言则利用选项类型
把模型核心内容通过尽可能少的代码展现出来。
此时你也许会有所疑虑,在OO的设计中,每种支付方式都是一个独立的实现,所以每种支付方式的具体行为就可以设计在具体的实现中,例如Payment的过程。不同的支付方式显然需要不同的支付过程,这种设计在OO中的好处是显而易见的。
在选项类型
中,似乎把三种不同的支付方式揉在了一块,那么每种支付方式的支付过程这种行为怎么实现呢?答案是模式匹配
,我们将在下节介绍。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述