函数式设计:代数数据类型 什么是ADT?乘积类型 和类型 函数式错误处理 结论

2025-06-08

功能设计:代数数据类型

什么是 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]

这里HourPeriod是独立的,即的值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
PREV
开始使用 fp-ts:应用应用柯里化应用应用提升一般问题解决了吗?
NEXT
如何使用 Node 和 Express 将客户端连接到服务器端。前端设置 后端设置 让我们直观地了解测试时间