【F#初学者】第三讲 可区分联合与其高级运用

在上一讲中,我们了解了 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值