Algebra Data Types,代数数据类型,是一个函数式编程中非常有趣的概念,从 java 角度来看,ADT 可以认为是一种模式,它看着就像带上数据的枚举类型 ,似乎平平无奇,但使用代数数据类型能让我们更好地进行类型建模 ,表达更多的东西并避免运行时异常(典型如空指针)。
问题在哪里? 考虑这样的一个(简化的)场景,我们要去实现一个 TODO APP,其中每条 TODO 称为 Task,Task 有如下性质:
Task 有四种状态:已完成,未完成 
Task 有两种种类:某日的 Task,某日期区间的 Task(即该 Task 在某日期区间内都持续进行,比如进行三天的旅行,摸鱼一个星期……) 
 
于是,设计出数据库表后,我们编写了这样的实体(Java 语言描述):
enum  TaskStatus  {enum  TaskType  {class  Task  {
Java 开发时一般来说就是这么干的(枚举可能换成数据字典项啥的以节约空间,方便序列化),但这里有一个严重问题:
一些字段和其它字段是相互关联的,并且这种关联性并未在类型定义上体现出来 ,比如对于 startDate 和 endDate,其显然在 type 为 DURATION 的时候才有意义即非 null;对于 taskDate,其只在 type 为 DAY 时有意义;对于 doneDate,其只在 status 为 DONE 时有意义……这种关联性必须通过文档,注释等手段去表达 。
而且,要获取这些相关联的数据时,必须要先对 type 和 status 进行判断才能保证正确,但若程序员因不熟悉业务等情况忘记进行判断会如何?bug 可能就要出现了,而编译器并管不了这个。
Java 中的 ADT 解决方案呢?我们可以让 Task 成为一个富血对象,严格限制访问域,在 getter,setter 中去实现约束,比如下面实现一个 startDate 的 getter:
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  {public  static  class  TaskStatusNotDone  extends  TaskStatus  {}class  Task  {
“带上数据的枚举类型”!在实际操作时,对 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): Aabstract  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> = {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  orElseelse  if  (this .isJust ()) return  this .value throw  new  Error ('Impossible' )return  optionalexport  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