Algebra Data Types,代数数据类型,是一个函数式编程中非常有趣的概念,从 java 角度来看,ADT 可以认为是一种模式,它看着就像带上数据的枚举类型 ,似乎平平无奇,但使用代数数据类型能让我们更好地进行类型建模 ,表达更多的东西并避免运行时异常(典型如空指针)。
问题在哪里? 考虑这样的一个(简化的)场景,我们要去实现一个 TODO APP,其中每条 TODO 称为 Task,Task 有如下性质:
Task 有四种状态:已完成,未完成
Task 有两种种类:某日的 Task,某日期区间的 Task(即该 Task 在某日期区间内都持续进行,比如进行三天的旅行,摸鱼一个星期……)
于是,设计出数据库表后,我们编写了这样的实体(Java 语言描述):
enum TaskStatus { DONE, NOT_DONE }enum TaskType { DAY, DURATION }class Task { Integer id; TaskStatus status; TaskType type; Date taskDate; Date startDate; Date endDate; Date doneDate; }
Java 开发时一般来说就是这么干的(枚举可能换成数据字典项啥的以节约空间,方便序列化),但这里有一个严重问题:
一些字段和其它字段是相互关联的,并且这种关联性并未在类型定义上体现出来 ,比如对于 startDate 和 endDate,其显然在 type 为 DURATION 的时候才有意义即非 null;对于 taskDate,其只在 type 为 DAY 时有意义;对于 doneDate,其只在 status 为 DONE 时有意义……这种关联性必须通过文档,注释等手段去表达 。
而且,要获取这些相关联的数据时,必须要先对 type 和 status 进行判断才能保证正确,但若程序员因不熟悉业务等情况忘记进行判断会如何?bug 可能就要出现了,而编译器并管不了这个。
Java 中的 ADT 解决方案呢?我们可以让 Task 成为一个富血对象,严格限制访问域,在 getter,setter 中去实现约束,比如下面实现一个 startDate 的 getter:
Optional<Date> getStartDate () { if (this .type == TaskType.DURATION) return Optional.empty(); return Optional.of(this .startDate); }
这能强迫使用者去关心相关的约束,但仍旧显得很繁琐,遗憾的是在 Java 添加 sealed 关键字和模式匹配之前,将 ADT 应用在业务代码上确实没有多少合适的解决方案。
一个可能的解决方案是什么呢?我们可以把和 Task 的状态相关的字段和这个状态绑在一起,比如对于 TaskStatus 这个状态,我们可以为每个情况定义相应 Class:
abstract class TaskStatus { private TaskStatus () {} public static class TaskStatusDone extends TaskStatus { Date doneDate; } public static class TaskStatusNotDone extends TaskStatus {} }class Task { TaskStatus status; }
“带上数据的枚举类型”!在实际操作时,对 status 就必须使用 instanceof 去判断它的类型了,判断后进行类型强转即可获取其值。
但这里仍旧有一个问题:强转是向下转型,仍旧需要使用者去确定强转的类型,且 IDE 不一定补全正确的类型 ,因此有一定心智负担,且仍旧可能出错。
一个简单的优化方案是在 TaskStatus 中添加相应方法去获取数据:
abstract class TaskStatus { public Optional<Date> getDoneDate () { if (this instanceof TaskStatusDone) { return Optional.of(((TaskStatusDone) this ).doneDate); } return Optional.empty(); } }
在 Kotlin,Scala,Typescript 语言中去实现的 ADT 实际上在很大程度上和这种方法(模式)是一致的,但是搭配上这些语言的模式匹配 功能(以及 sealed, case 等关键字),用起来就会顺滑无比,从而真正在工程实践中有应用价值。
Typescript 中的 ADT 在 Typescript 中去实现 ADT 轻而易举,因为它本身就包含所谓的或(积 Product)类型 (|
运算符),比如上面的 TaskStatus 可以直接这么表达:
type TaskNotDone = { _tag : 'TaskNotDone' }type TaskDone = { _tag : 'TaskDone' , doneDate : Date }export type TaskStatus = TaskDone | TaskNotDone const status : TaskStatus = switch (status._tag ) { case 'TaskNotDone' : break ; case 'TaskDone' : }
Typescript 足够聪明,用户只需要对 _tag 进行判断,它就能够知晓该数据的类型究竟是 TaskDone 还是 TaskNotDone ,因此我们可以用 switch 去做简单的模式匹配(并且输入 case 的时候也能得到补全!)。
React 的 Reducer 的 Action 类型也可以看作 ADT,这时它的 type 属性就代表它的类型:
type SomeReducerAction = { type : 'INC' , inc : number } | { type : 'DEC' , dec : number } | { type : 'SET' , v : number }
但在这里有一个问题——这样操作的话没法往 ADT 上面添加方法了,这对面向对象语言还是非常难受的,但仍旧可以解决,如下面的代码实现 Typescript 版的 Maybe:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 type Nothing = { _tag : 'Nothing' }type Just <A> = { _tag : 'Just' , value : A }type Optional <A> = (Nothing | Just <A>) & OptMethods <A>abstract class OptMethods <A> { abstract isNothing (): this is Nothing abstract isJust (): this is Just <A> abstract map<B>(fn : (a: A ) => B): Optional <B> abstract orElse (orElse : A): A abstract flatMap<B>(fn : (a: A ) => Optional <B>): Optional <B> }function mkOptional<A>(value?: A | null | undefined ) : Optional <A> { const data : Nothing | Just <A> = value ? {_tag : 'Just' , value} : {_tag : 'Nothing' } const optional : Optional <A> = { ...data, isNothing ( ) { return this ._tag === 'Nothing' }, isJust ( ) { return this ._tag === 'Just' }, map (fn ) { if (this .isJust ()) return mkOptional (fn (this .value )) else if (this .isNothing ()) return mkOptional () throw new Error ('Impossible' ) }, flatMap (fn ) { if (this .isNothing ()) return mkOptional () else if (this .isJust ()) return fn (this .value ) throw new Error ('Impossible' ) }, orElse (orElse ) { if (this .isNothing ()) return orElse else if (this .isJust ()) return this .value throw new Error ('Impossible' ) } } return optional }export function Just <A>(value : A): Optional <A> { return mkOptional (value) }export function Nothing <A>(): Optional <A> { return mkOptional () }
这就舒爽啦!
tips: Haskell 的 ADT 类型定义类似data Maybe a = Nothing | Just a
,其中 Maybe 称为类型构造器 ,Nothing 和 Just 对应值构造器 ,Maybe 对应这里的 Optional 类型,Nothing 和 Just 对应两个同名函数
参考资料 Algebraic Data Types in TypeScript