代数数据类型与领域建模

简介: 代数数据类型与领域建模

函数范式



REA的Ken Scambler认为函数范式的主要特征为:模块化(Modularity),抽象化(Abstraction)和可组合(Composability)。这三个特征可以帮助我们编写简单的程序。


通常,为了降低系统的复杂度,都需要将系统分解为多个功能的组成部分,每个组成部分有着清晰的边界。模块化的编码范式需要支持实现者能够轻易地对模块进行替换,这就要求模块具有隔离性,避免在模块之间出现太多的纠缠。函数范式以“函数”为核心,作为模块化的重要组成部分。函数范式要求函数均为没有副作用的纯函数(pure function)。在推断每个函数的功能时,由于函数没有产生副作用,就可以不考虑该函数当前所处的上下文,形成清晰的隔离边界。这种相互隔离的纯函数使得模块化成为可能。


函数的抽象能力不言而喻,因为它本质上是一种将输入类型转换为输出类型的转换行为。任何一个函数都可以视为一种转换(transform),这是对行为的最高抽象,代表了类型(type)之间的某种动作。极端情况下,我们甚至不用考虑函数的名称和类型,只需要关注其数学本质:f(x) = y。其中,x是输入,y是输出,f就是极度抽象的函数。


函数范式领域模型的核心要素为代数数据类型(Algebraic Data Type, ADT)和纯函数。代数数据类型表达领域概念,纯函数表达领域行为。由于二者皆被定义为不变的、原子的,因此在类型的约束规则下可以对它们进行组合。可组合的特征使得函数范式建立的领域模型可以由简单到复杂,利用组合子来表现复杂的领域逻辑。


代数数据类型



代数数据类型借鉴了代数学中的概念,作为一种函数式数据结构,体现了函数范式的数学意义。通常,代数数据类型不包含任何行为。它利用和类型(Sum Type)来展示相同抽象概念的不同组合,使用积类型(Product Type)来展示同一个概念不同属性的组合。


和与积是代数中的概念,它们在函数范式中体现了类型的两种组合模式。和就是加,用以表达一种类型是它的所有子类型之和。例如表达时间单位的TimeUnit类型:


sealed trait TimeUnit
case object Days extends TimeUnit
case object Hours extends TimeUnit
case object Minutes extends TimeUnit
case object Seconds extends TimeUnit
case object MilliSeconds extends TimeUnit
case object MicroSeconds extends TimeUnit
case object NanoSeconds extends TimeUnit


在上述模型中,TimeUnit是对时间单位概念的一个抽象。定义为和类型,说明它的实例只能是以下的任意一种:Days、Hours、Minutes、Seconds、MilliSeconds、MicroSeconds或NanoSeconds。这是一种逻辑或的关系,用加号来表示:


type TimeUnit = Days + Hours + Minutes + Seconds + MilliSeconds + MicroSeconds + NanoSeconds


积类型体现了一个代数数据类型是其属性组合的笛卡尔积,例如一个员工类型:


case class Employee(number: String, name: String, email: String, onboardingDate: Date)


它表示Employee类型是(String, String, String, Date)组合的集合,也就是这四种数据类型的笛卡尔积,在类型语言中可以表达为:


type Employee = (String, String, String, Date)


也可以用乘号来表示这个类型的定义:


type Employee = String * String * String * Date


和类型和积类型的这一特点体现了代数数据类型的组合(combinable)特性。代数数据类型的这两种类型并非互斥的,有的代数数据类型既是和类型,又是积类型,例如银行的账户类型:


sealed trait Currency
case object RMB extends Currency
case object USD extends Currency
case object EUR extends Currency
case class Balance(amount: BigDecimal, currency: Currency)
sealed trait Account {
  def number: String
  def name: String
}
case class SavingsAccount(number: String, name: String, dateOfOpening: Date) extends Account
case class BilledAccount(number: String, name: String, dateOfOpening: Date, balance: Balance) extends Account


代码中的Currency被定义为和类型,Balance为积类型。Account首先是和类型,它的值要么是SavingsAccount,要么是BilledAccount;同时,每个类型的Account又是一个积类型。


代数数据类型与对象范式的抽象数据类型有着本质的区别。前者体现了数学计算的特性,具有不变性。使用Scala的case object或case class语法糖会帮助我们创建一个不可变的抽象。当我们创建了如下的账户对象时,它的值就已经确定,不可改变:


val today = Calendar.getInstance.getTime
val balance = Balance(10.0, RMB)
val account = BilledAccount("980130111110043", "Bruce Zhang", today, balance)


数据的不变性使得代码可以更好地支持并发,可以随意共享值而无需承受对可变状态的担忧。不可变数据是函数式编程中实践的重要原则之一,它可以与纯函数更好地结合。


代数数据类型既体现了领域概念的知识,同时还通过和类型和积类型定义了约束规则,从而建立了严格的抽象。例如类型组合(String, String, Date)是一种高度的抽象,但它却丢失了领域知识,因为它缺乏类型标签,如果采用积类型方式进行定义,则在抽象的同时,还约束了各自的类型。和类型在约束上更进了一步,它将变化建模在一个特定数据类型内部,并限制了类型的取值范围。和类型与积类型结合起来,与操作代数数据类型的函数放在一起,然后利用模式匹配来实现表达业务规则的领域行为。


我们以Robert Martin在《敏捷软件开发》一书中给出的薪资管理系统需求为例,利用函数范式的建模方式来说明代数数据类型的优势。需求描述如下:


公司雇员有三种类型。一种雇员是钟点工,系统会按照雇员记录中每小时报酬字段的值对他们进行支付。他们每天会提交工作时间卡,其中记录了日期以及工作小时数。如果他们每天工作超过8小时,超过部分会按照正常报酬的1.5倍进行支付。支付日期为每周五。月薪制的雇员以月薪进行支付。每个月的最后一个工作日对他们进行支付。在雇员记录中有月薪字段。销售人员会根据他们的销售情况支付一定数量的酬金(Commssion)。他们会提交销售凭条,其中记录了销售的日期和数量。在他们的雇员记录中有一个酬金报酬字段。每隔一周的周五对他们进行支付。


我们现在要计算公司雇员的薪资。从需求看,我们需要建立的领域模型是雇员,它是一个积类型。注意,需求虽然清晰地勾勒出三种类型的雇员,但实则它们的差异体现在收入的类型上,这种差异体现为和类型不同的值。于是,可以得到由如下代数数据类型呈现的领域模型:


// ADT定义,体现了领域概念
// Amount是一个积类型,Currency则为前面定义的和类型
calse class Amount(value: BigDecimal, currency: Currency) {
  // 实现了运算符重载,支持Amount的组合运算
  def +(that: Amount): Amount = {
    require(that.currency == currency)
    Amount(value + that.value, currency)
  }
  def *(times: BigDecimal): Amount = {
    Amount(value * times, currency)
  }
}
// 以下类型皆为积类型,分别体现了工作时间卡与销售凭条领域概念
case class TimeCard(startTime: Date, endTimeDate)
case class SalesReceipt(date: Date, amount: Amount)
// 支付周期是一个隐藏概念,不同类型的雇员支付周期不同
case class PayrollPeriod(startDate: Date, endDate: Date)
// Income的抽象表示成和类型与乘积类型的组合
sealed trait Income
case class WeeklySalary(feeOfHour: Amount, timeCards: List[TimeCard], payrollPeriod: PayrollPeriod) extends Income
case class MonthlySalary(salary: Amount, payrollPeriod: PayrollPeriod) extends Income
case class Commission(salary: Amount, saleReceipts: List[SalesReceipt], payrollPeriod: PayrollPeriod)
// Employee被定义为积类型,它组合的Income具有不同的抽象
case class Employee(number: String, name: String, onboardingDate: Date, income: Income)


在定义了以上由代数数据类型组成的领域模型之后,即可将其与领域行为结合起来。


例如计算每个雇员的收入,由于Income被定义为和类型,它表达的是一种逻辑或的关系,因此它的每个子类型(称为ADT变体)都将成为模式匹配的分支。和类型的组合有着确定的值(类型理论的术语将其称之为inhabitant),例如Income和类型的值为3,则模式匹配的分支就应该是3个,这就使得Scala编译器可以检查模式匹配的穷尽性。


如果模式匹配缺少了对和类型的值表示,编译器都会给出警告。倘若和类型增加了一个新的值,编译器也会指出所有需要新增ADT变体来更新模式匹配的地方。针对Income积类型,可以利用模式匹配结合业务规则对它进行解构,代码如下所示:


def calculateIncome(employee: Employee): Amount = employee.income match {
  case WeeklySalary(fee, timeCards, _) => weeklyIncomeOf(fee, timeCards)
  case MonthlySalary(salary, _) => salary
  case Commision(salary, saleReceipts, _) => salary + commistionOf(saleReceipts)
}


利用模式匹配,针对Employee的特定Income类型可以计算雇员的不同收入。

相关文章
|
Java 安全
一文读懂Java泛型中的通配符 ?
之前不太明白泛型中通配符"?"的含义,直到我在网上发现了Jakob Jenkov的一篇文章,觉得很不错,所以翻译过来,大家也可以点击文末左下角的阅读原文看英文版的原文。 下面是我的译文: Java泛型中的通配符机制的目的是:让一个持有特定类型(比如A类型)的集合能够强制转换为持有A的子类或父类型的集合,这篇文章将解释这个是如何做的。
14887 2
|
存储 前端开发 索引
【面试题】数组去重的五种方法(必会)
【面试题】数组去重的五种方法(必会)
581 1
|
9月前
|
机器学习/深度学习 人工智能 测试技术
HumanOmniV2 比你还懂“社交潜台词”!
如何让AI真正“读懂”人心?本文通过分析相亲对话案例,揭示当前多模态大模型在全局上下文理解和深度逻辑推理上的不足,并介绍全新模型HumanOmniV2的创新机制,如强制性上下文总结和多维度奖励机制,显著提升AI对人类复杂意图的理解能力。
303 1
|
8月前
|
人工智能 安全 网络安全
2025攻防演习回顾,AI赋能下的网络安全新格局
网络安全实战攻防演习历经9年发展,已成为检验安全体系、洞察威胁趋势的重要手段。攻击呈现实战化、体系化特征,APT、0day、勒索攻击等手段升级,AI、大数据等新技术带来新风险。攻击入口多元化、工具智能化、API成重点目标,“AI+人工”协同攻击加剧威胁。面对挑战,企业需构建纵深防御体系,从被动防御转向主动对抗。瑞数信息通过动态安全技术与AI融合,实现0day防护、漏扫干扰、勒索应急等能力,打造WAAP超融合平台,助力关键基础设施构建智能、协同、前瞻的主动防御体系。
677 1
|
11月前
|
机器学习/深度学习 存储 安全
4G手机内存玩转Qwen2.5-Omni?MNN全面支持Qwen2.5-Omni与Qwen3!
随着移动端算力、存储能力的提升,在端侧部署大模型已成为趋势。本地化运行可消除网络延迟实现毫秒响应,降低云端算力成本,同时避免数据上传保障隐私安全。
2493 1
|
安全 API Android开发
Android 15: 迈向64位时代的重大更新与全新体验
2024年,Google发布Android 15,迈向64位计算新时代。新系统淘汰32位应用,引入多项性能优化与新特性,如矢量emoji、预测性返回动画等,并预计随Pixel 9系列一同发布。开发者需更新应用确保兼容性,并利用新功能提升用户体验。
3886 15
Android 15: 迈向64位时代的重大更新与全新体验
|
8月前
|
存储 搜索推荐 API
电商 API 开启多平台营销推广数据整合新玩法
在数字化时代,电商企业面临多平台数据分散、分析效率低等问题。电商 API 通过整合淘宝、京东等平台数据,实现统一数据源、实时同步与精准营销,助力企业提升决策效率,开启个性化推荐、自动化营销等新玩法,是驱动营销升级的关键工具。
291 0
|
编解码 安全 Linux
网络空间安全之一个WH的超前沿全栈技术深入学习之路(10-2):保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali——Liinux-Debian:就怕你学成黑客啦!)作者——LJS
保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali以及常见的报错及对应解决方案、常用Kali功能简便化以及详解如何具体实现
|
安全 Java 程序员
Zig 内存管理
Zig 内存管理
466 1
|
存储 搜索推荐 索引
5个 Elasticsearch 核心组件
Elasticsearch 是基于 Lucene 的分布式搜索引擎,具备高可用和多租户特性。其核心组件包括:节点(Node)、集群(Cluster)、索引(Index)、分片(Shard)和副本(Replica)。节点是集群中的服务器,可设置为主、数据或客户端节点。集群由多个节点组成,通过集群名称区分。索引是文档集合,7.x 版本后每个索引仅含一种类型。分片是索引的子集,可分布于不同节点,分为主分片和副本分片,副本用于提高数据可用性和性能。【5月更文挑战第5天】
456 1

热门文章

最新文章

下一篇
开通oss服务