功能设计:代数数据类型
什么是 ADT?
产品类型
和类型
功能错误处理
结论
构建新应用程序的第一步是定义其领域模型。TypeScript 提供了许多工具来帮助你完成这项任务。代数数据类型(简称 ADT)就是其中之一。
什么是 ADT?
在计算机编程中,尤其是函数式编程和类型理论中,代数数据类型是一种复合类型,即由其他类型组合而成的类型。
两种常见的代数类型是:
- 产品类型
- 总和类型
产品类型
产品类型是通过集合索引的类型 T i的集合I
。
这个家族的两个常见成员是n
-tuples,其中I
是自然数的非空区间……
type Tuple1 = [string] // I = [0]
type Tuple2 = [string, number] // I = [0, 1]
type Tuple3 = [string, number, boolean] // I = [0, 1, 2]
// Accessing by index
type Fst = Tuple2[0] // string
type Snd = Tuple2[1] // number
...和结构,其中I
是一组标签。
// I = {"name", "age"}
interface Person {
name: string
age: number
}
// Accessing by label
type Name = Person['name'] // string
type Age = Person['age'] // number
为什么是“产品”类型?
如果我们将该C(A)
类型的居民数量A
(也称为基数)写为,则以下等式成立
C([A, B]) = C(A) * C(B)
乘积的基数是基数的乘积
例子
type Hour = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
type Period = 'AM' | 'PM'
type Clock = [Hour, Period]
该Clock
类型有12 * 2 = 24
居民。
我应该何时使用产品类型?
只要其组成部分都是独立的。
type Clock = [Hour, Period]
这里Hour
和Period
是独立的,即的值Hour
不会影响的值Period
,反之亦然,所有对都是合法且有意义的。
和类型
和类型是一种数据结构,用于保存一个值,该值可以采用几种不同但固定的类型。任何时刻只能使用其中一种类型,并且标记字段会明确指示正在使用哪种类型。
在 TypeScript 文档中,它们被命名为标记联合类型。
示例(redux 操作)
type Action =
| {
type: 'ADD_TODO'
text: string
}
| {
type: 'UPDATE_TODO'
id: number
text: string
completed: boolean
}
| {
type: 'DELETE_TODO'
id: number
}
该type
字段是标签并确保其成员是不相交的。
构造函数
具有成员的 sum 类型n
需要n
构造函数,每个成员一个
const add = (text: string): Action => ({
type: 'ADD_TODO',
text
})
const update = (id: number, text: string, completed: boolean): Action => ({
type: 'UPDATE_TODO',
id,
text,
completed
})
const del = (id: number): Action => ({
type: 'DELETE_TODO',
id
})
和类型可以是多态的和/或递归的。
示例(链接列表)
// ↓ type parameter
type List<A> = { type: 'Nil' } | { type: 'Cons'; head: A; tail: List<A> }
// ↑ recursion
模式匹配
JavaScript 没有模式匹配fold
(TypeScript 也没有),但是我们可以通过定义一个函数来定义一个“穷人”模式匹配
const fold = <A, R>(
fa: List<A>,
onNil: () => R,
onCons: (head: A, tail: List<A>) => R
): R => (fa.type === 'Nil' ? onNil() : onCons(fa.head, fa.tail))
示例(递归计算的长度List
)
const length = <A>(fa: List<A>): number =>
fold(fa, () => 0, (_, tail) => 1 + length(tail))
为什么是“总和”类型?
以下等式成立
C(A | B) = C(A) + C(B)
总和的基数是基数的总和
示例(Option
类型)
type Option<A> =
| { type: 'None' }
| {
type: 'Some'
value: A
}
C(Option<A>) = 1 + C(A)
例如,我们可以从一般公式推导出Option<boolean>
:1 + 2 = 3
居民的基数。
我什么时候应该使用总和类型?
如果将其作为产品类型实现,其组件将会相互依赖。
示例(组件道具)
interface Props {
editable: boolean
onChange?: (text: string) => void
}
class Textbox extends React.Component<Props> {
render() {
if (this.props.editable) {
// error: Cannot invoke an object which is possibly 'undefined' :(
this.props.onChange(...)
}
}
}
这里的问题是,Props
被建模为产品类型,但onChange
取决于editable
。
和类型是更好的选择
type Props =
| {
type: 'READONLY'
}
| {
type: 'EDITABLE'
onChange: (text: string) => void
}
class Textbox extends React.Component<Props> {
render() {
switch (this.props.type) {
case 'EDITABLE' :
this.props.onChange(...) // :)
...
}
}
}
示例(节点回调)
declare function readFile(
path: string,
// ↓ ---------- ↓ CallbackArgs
callback: (err?: Error, data?: string) => void
): void
结果被建模为产品类型
type CallbackArgs = [Error | undefined, string | undefined]
然而,它的组件是相互依赖的:你要么得到一个错误,要么得到一个字符串
犯错 | 数据 | 合法的? |
---|---|---|
Error |
undefined |
✓ |
undefined |
string |
✓ |
Error |
string |
✘ |
undefined |
undefined |
✘ |
总和类型是更好的选择,但是哪一个呢?
功能错误处理
让我们看看如何以函数式风格处理错误。
类型Option
该Option
类型表示计算可能失败或产生以下类型的值A
type Option<A> =
| { type: 'None' } // represents a failure
| { type: 'Some'; value: A } // represents a success
构造函数和模式匹配:
// a nullary constructor can be implemented as a constant
const none: Option<never> = { type: 'None' }
const some = <A>(value: A): Option<A> => ({ type: 'Some', value })
const fold = <A, R>(fa: Option<A>, onNone: () => R, onSome: (a: A) => R): R =>
fa.type === 'None' ? onNone() : onSome(fa.value)
该Option
类型可用于避免抛出异常和/或表示可选值,因此我们可以从...开始
// this is a lie ↓
const head = <A>(as: Array<A>): A => {
if (as.length === 0) {
throw new Error('Empty array')
}
return as[0]
}
let s: string
try {
s = String(head([]))
} catch (e) {
s = e.message
}
...类型系统不知道可能发生的故障,所以...
// ↓ the type system "knows" that this computation may fail
const head = <A>(as: Array<A>): Option<A> => {
return as.length === 0 ? none : some(as[0])
}
const s = fold(head([]), () => 'Empty array', a => String(a))
...失败的可能性被提升到类型系统。
类型Either
一种常见的用法Either
是作为 的替代,用于Option
处理可能缺失的值。在这种用法中,None
被 替换,Left
后者可以包含有用的信息。Right
代替Some
。惯例规定 表示Left
失败,Right
表示成功。
type Either<L, A> =
| { type: 'Left'; left: L } // represents a failure
| { type: 'Right'; right: A } // represents a success
构造函数和模式匹配:
const left = <L, A>(left: L): Either<L, A> => ({ type: 'Left', left })
const right = <L, A>(right: A): Either<L, A> => ({ type: 'Right', right })
const fold = <L, A, R>(
fa: Either<L, A>,
onLeft: (left: L) => R,
onRight: (right: A) => R
): R => (fa.type === 'Left' ? onLeft(fa.left) : onRight(fa.right))
回到我们的回调示例
declare function readFile(
path: string,
callback: (err?: Error, data?: string) => void
): void
readFile('./myfile', (err, data) => {
let message: string
if (err !== undefined) {
message = `Error: ${err.message}`
} else if (data !== undefined) {
message = `Data: ${data.trim()}`
} else {
// should never happen
message = 'The impossible happened'
}
console.log(message)
})
我们可以将其签名更改为
declare function readFile(
path: string,
callback: (result: Either<Error, string>) => void
): void
然后像这样使用 API
readFile('./myfile', e => {
const message = fold(e, err => `Error: ${err.message}`, data => `Data: ${data.trim()}`)
console.log(message)
})
结论
在这篇文章中,我们看到了产品类型和总和类型,以及根据它们所代表的状态数量进行推理如何极大地影响我们的领域模型的设计。
许多现实世界 API 的一个常见缺陷是滥用产品类型,除了所有合法状态之外,还模拟了许多非法状态。
和类型是一种非常有用且基本的语言特性,它们是设计优秀领域模型的关键,因为它允许使非法状态无法表示。
鏂囩珷鏉ユ簮锛�https://dev.to/gcanti/function-design-algebraic-data-types-36kf