代数数据类型与领域建模

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

函数范式



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的子类或父类型的集合,这篇文章将解释这个是如何做的。
14424 2
|
安全 Java API
解决 Swagger API 未授权访问漏洞:完善分析与解决方案
Swagger 是一个用于设计、构建、文档化和使用 RESTful 风格的 Web 服务的开源软件框架。它通过提供一个交互式文档页面,让开发者可以更方便地查看和测试 API 接口。然而,在一些情况下,未经授权的访问可能会导致安全漏洞。本文将介绍如何解决 Swagger API 未授权访问漏洞问题。
|
11月前
|
编解码 安全 Linux
网络空间安全之一个WH的超前沿全栈技术深入学习之路(10-2):保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali——Liinux-Debian:就怕你学成黑客啦!)作者——LJS
保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali以及常见的报错及对应解决方案、常用Kali功能简便化以及详解如何具体实现
|
10月前
|
JSON 自然语言处理 Java
OpenAI API深度解析:参数、Token、计费与多种调用方式
随着人工智能技术的飞速发展,OpenAI API已成为许多开发者和企业的得力助手。本文将深入探讨OpenAI API的参数、Token、计费方式,以及如何通过Rest API(以Postman为例)、Java API调用、工具调用等方式实现与OpenAI的交互,并特别关注调用具有视觉功能的GPT-4o使用本地图片的功能。此外,本文还将介绍JSON模式、可重现输出的seed机制、使用代码统计Token数量、开发控制台循环聊天,以及基于最大Token数量的消息列表限制和会话长度管理的控制台循环聊天。
3361 7
|
12月前
|
关系型数据库 MySQL 数据库
一个 MySQL 数据库死锁的案例和解决方案
本文介绍了一个 MySQL 数据库死锁的案例和解决方案。
719 3
|
11月前
|
机器学习/深度学习 自然语言处理 PyTorch
Transformers入门指南:从零开始理解Transformer模型
【10月更文挑战第29天】作为一名机器学习爱好者,我深知在自然语言处理(NLP)领域,Transformer模型的重要性。自从2017年Google的研究团队提出Transformer以来,它迅速成为NLP领域的主流模型,广泛应用于机器翻译、文本生成、情感分析等多个任务。本文旨在为初学者提供一个全面的Transformers入门指南,介绍Transformer模型的基本概念、结构组成及其相对于传统RNN和CNN模型的优势。
10116 1
|
SQL C++
数仓之归因分析
数仓之归因分析
|
开发框架 .NET C#
浅谈c和c++和c#之间的关系
浅谈c和c++和c#之间的关系
367 0
|
网络协议 Java 应用服务中间件
什么是tomcat?tomcat是干什么用的?
什么是tomcat?tomcat是干什么用的?
|
前端开发 Java 关系型数据库
手办商城系统|Springboot+vue+ElementUI手办商城系统
手办商城系统|Springboot+vue+ElementUI手办商城系统
273 0