在上一讲中,我们了解了 F# 的类型系统,其中有一个特殊的类型——可区分联合(Discriminated Unions)。这种类型不仅能够表示多种可能的值,还能为每个值附加额外的数据,这使得它在函数式编程中非常强大且灵活。
今天,我们将深入探讨可区分联合的更多可能性,并展示如何通过它来实现更高级的编程模式,(在函数式编程的第四讲中我们将详细的介绍monad, 本系列更注重语言的本身)
不过最开始,我们还是来介绍可区分联合的基本用法
可区分联合可以用于简单的替代一些小型对象的结构,不使用Shape继承派生出多种类
type Shape =
| Rectangle of float * float
| Circle of float
| Prism of float * float * float
let rect = Rectangle(10.0, 1.3)
当需要计算不同Shape的面积的函数时,无需使用虚方法重写,转而直接使用模式匹配,在不同的分支去实现其对应的计算即可
let getShapeSize shape =
match shape with
| Rectangle(width, length) -> width * length
| Circle(radius) -> 3.14 * radius * radius
| Prism(width, length, height) -> width * length * height
单一模式匹配的函数,还有以下方法可以简化编写
let getShapeSize = function
| Rectangle(width, length) -> width * length
| Circle(radius) -> 3.14 * radius * radius
| Prism(width, length, height) -> width * length * height
在以上的基础上,我们可以给参数加上自己的名字(Remarks)
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
(请注意Prism是3个float组成元组,其中第一个参数和第三个参数有名字)
加上这个标记后,我们在初始化与模式匹配时,就可以更准确的指明某一个参数
let rect = Rectangle(length = 1.3, width = 10.0)
let rect1 = Rectangle(width = 1.1, length = 1.4)
let prism = Prism(5., 2.0, height = 3.0)
let getShapeWidth = function
| Rectangle(width = w) -> w
| Circle(radius = r) -> 2. * r
| Prism(width = w) -> w
递归类型
可区分联合本身是可以递归定义的类型,也就是说我们可以做到这种事情
type Any = | A of Any
我们定义了一个Any类型,他唯一的构造需要一个Any类型,这就导致这个类型在正常情况下无法被初始化。
当然,这只是一个有趣的案例。递归类型能够做到更强大的事,我们稍作更改
type Any = | A of int * Any | B
这时候,原来的A构造器变为了一个int*Any的元组类型, B则是一个不需要任何参数构造器。这时候我们的递归有了终点,也就是这个B,我们有办法离开这个递归。
看着这个形式,我们好像想起了什么,灵机一动修改了一下他的名字,
type List = | Cons of int * List | Empty
如果读者有一定的数据结构基础,可能会发出惊呼“这不就是一个链表吗?”,一个节点拥有一个元素,和指向下一个节点(剩余的链表)的指针,毫无疑问,这就是一个链表。当然我们可以加入泛型,让他更泛用。
type List<'a> = | Cons of 'a * List<'a> | Empty
let a = Cons (1, Cons (2, Cons (3, Empty)))
这样我们就成功的用可区分联合定义了一个经典数据结构!
通过这种方式我们还可以轻松的构建其他的数据结构
type Tree<'a> =
| Node of 'a * left: Tree<'a> * right: Tree<'a>
| Empty
甚至可以轻松的构建一个表达式的语法树
type Expr =
| Number of int
| Add of Expr * Expr
| Subtract of Expr * Expr
| Multiply of Expr * Expr
| Divide of Expr * Expr
let a = Subtract (Add (Number 1, Number 3) , Multiply (Number 2, Number 3))
顺便这里布置一个小小的课后作业,请实现计算这个表达式的函数
let calc expr =
() // 课后作业
如果需要循环引用的话,需要用and来连续定义,以下是一个简单的文件系统的定义
type File =
| File of string * int // 文件(名称,大小)
and Folder =
| Folder of string * (FileSystemItem list)
and FileSystemItem =
| FileItem of File
| FolderItem of Folder
结构可区分联合
当加入[<Struct>]特性后,可区分联合便可以以值的形式存在,但这样也失去了递归定义的可能性
自动实现的is属性
在F#9中,可区分联合拥有了一个自动实现的属性,用于判断是否属于某种构造,例如
let flag = circle.IsCircle
需要手动指定最新的语言版本
为缺失的数据建模
看完上面的介绍后,不知读者是否发现了一个我的失误?
回到最开始,我们编写了一个getShapeSize的函数用于求shape的面积,但其实其中一个构造器表示的是一个3维的物体!虽然我们用求体积糊弄过去了,但其实违反了诚实性(当然,你也可以声明这个函数既可以求体积也可以求面积)。我们想了一想,既然3维的物体是没有面积的,我们能不能直接返回没有面积这个事实,思考再三,我们编写了如下的代码
type MaybeSize = | Size of double | NoSize
let getShapeSize2 = function
| Rectangle(width, length) -> Size (width * length)
| Circle(radius) -> Size (3.14 * radius * radius)
| Prism(width, length, height) -> NoSize
match getShapeSize2 rect with
| Size size -> printfn "Size is: %f" size
| NoSize -> printfn "No size"
我们用MaybeSize代表可能会有面积这一数据,他的取值范围是double + 1(这也就是多次提到的和类型),这下我们诚实的返回了我们想要的结果,也足够精确。也完全不需要使用C#中 用null来表达一个空数据(null是一个很特殊的类型(System.Void))
再让他变得泛用一些,我们就可以得到如下类型。
type Maybe<'a> = | Just of 'a | Nothing
是不是很眼熟呢?没错,在上一期的最后一讲中,我们提到了一个Option的类型
type Option<'a> =
| Some of 'a
| None
没错这两种类型完全一样,Option就是我们刚刚编写的Maybe, 事实上这只是不同的表达习惯,有些语言中喜欢使用Maybe,不过F#中默认使用的是option,这是一个官方支持的类型,同样的,还有一个result的类型
在F#中 只需要在类型后面添加option,就可以代表这个值可能是缺失的,这等价于Option<int>, 不过这样的写法显然更加轻量级
type Person = { Name: string; Age: int option }
这种类型虽然非常简单,容易理解,但是背后却有非常深的学问,关于函子和单子相关的内容,将会在函数式编程的系列中进行详细讲解!
学会了吗?还没学会,但马上就能学会了!
微信公众号: @scixing的炼丹房
Bilibili: @无聊的年 【F#入门】(二) 可区分联合及其高级运用_哔哩哔哩_bilibili