已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

Option

用 Option 表示可选值,既可以是存在(Some),也可以是缺失(None),并支持映射、组合与模式匹配等无缝操作。

Option 数据类型表示可选值。一个 Option<A> 要么是 Some<A>,包含一个类型为 A 的值;要么是 None,表示值的缺失。

你可以在以下场景中使用 Option

  • 用作初始值
  • 从并非对所有可能输入都有定义的函数(即「偏函数」,partial function)中返回值
  • 管理数据结构中的可选字段
  • 处理可选的函数参数

创建 Option

some

使用 Option.some 构造器创建一个持有类型 A 值的 Option

示例(创建一个带值的 Option)

import { Option } from "effect"

// An Option holding the number 1
const value = Option.some(1)

console.log(value)
value // => Option.some(1)

none

使用 Option.none 构造器创建一个表示值缺失的 Option

示例(创建一个没有值的 Option)

import { Option } from "effect"

// An Option holding no value
const noValue = Option.none()

console.log(noValue)
noValue // => Option.none()

liftPredicate

你可以基于谓词创建 Option,例如检查一个值是否为正数。

示例(显式创建 Option)

下面展示如何用 Option.noneOption.some 实现这一点:

import { Option } from "effect"

const isPositive = (n: number) => n > 0

const parsePositive = (n: number): Option.Option<number> =>
  isPositive(n) ? Option.some(n) : Option.none()

parsePositive(5) // => Option.some(5)

示例(用 Option.liftPredicate 让代码更简洁)

或者,你可以用 Option.liftPredicate 简化上面的逻辑:

import { Option } from "effect"

const isPositive = (n: number) => n > 0

//      ┌─── (b: number) => Option<number>
//      ▼
const parsePositive = Option.liftPredicate(isPositive)

parsePositive(5) // => Option.some(5)

为可选属性建模

考虑一个 User 模型,其中 "email" 属性是可选的,可以保存 string 值。我们用 Option<string> 类型来表示这个可选属性:

import { Option } from "effect"

interface User {
  readonly id: number
  readonly username: string
  readonly email: Option.Option<string>
}
Property Key Always Present

可选性只作用于属性的值。键 "email" 无论是否有值, 都始终存在于对象中。

下面的示例展示了如何创建带 email 和不带 email 的 User 实例:

示例(创建带 email 和不带 email 的 User)

import { Option } from "effect"

interface User {
  readonly id: number
  readonly username: string
  readonly email: Option.Option<string>
}

const withEmail: User = {
  id: 1,
  username: "john_doe",
  email: Option.some("john.doe@example.com"),
}

const withoutEmail: User = {
  id: 2,
  username: "jane_doe",
  email: Option.none(),
}

withEmail.email // => Option.some("john.doe@example.com")
withoutEmail.email // => Option.none()

Guards

你可以使用 Option.isSomeOption.isNone 这两个 guard 检查一个 OptionSome 还是 None

示例(用 Guards 检查 Option 的值)

import { Option } from "effect"

const foo = Option.some(1)

console.log(Option.isSome(foo))
Option.isSome(foo) // => true

if (Option.isNone(foo)) {
  console.log("Option is empty")
} else {
  console.log(`Option has a value: ${foo.value}`)
}
// Output: "Option has a value: 1"
Option.getOrThrow(foo) // => 1

模式匹配

使用 Option.match 处理 Option 的两种情况:分别为 NoneSome 指定独立的回调。

示例(对 Option 进行模式匹配)

import { Option } from "effect"

const foo = Option.some(1)

const message = Option.match(foo, {
  onNone: () => "Option is empty",
  onSome: (value) => `Option has a value: ${value}`,
})

console.log(message)
message // => "Option has a value: 1"

使用 Option

map

Option.map 函数让你无需手动解包再重新包装,就能转换 Option 内部的值。如果 Option 持有值(Some),就应用该转换函数。如果 OptionNone,则忽略该函数,Option 保持不变。

示例(映射 Some 中的值)

import { Option } from "effect"

// Transform the value inside Some
console.log(Option.map(Option.some(1), (n) => n + 1))
Option.map(Option.some(1), (n) => n + 1) // => Option.some(2)

处理 None 时,映射函数不会执行,Option 仍然是 None

示例(对 None 进行映射)

import { Option } from "effect"

// Mapping over None results in None
console.log(Option.map(Option.none(), (n) => n + 1))
Option.map(Option.none(), (n) => n + 1) // => Option.none()

flatMap

Option.flatMap 函数与 Option.map 类似,但它用于处理转换可能返回另一个 Option 的情况。这让我们能够串联那些依赖于 Option 中是否存在值的计算。

考虑一个 User 模型,它包含嵌套的可选 Address,而 Address 自身又包含可选的 street 属性:

import { Option } from "effect"

interface User {
  readonly id: number
  readonly username: string
  readonly email: Option.Option<string>
  readonly address: Option.Option<Address>
}

interface Address {
  readonly city: string
  readonly street: Option.Option<string>
}

在这个模型中,address 字段是 Option<Address>,而 Address 中的 street 字段是 Option<string>

我们可以用 Option.flatMapaddress 中提取 street 属性:

示例(提取嵌套的可选属性)

import { Option } from "effect"

interface Address {
  readonly city: string
  readonly street: Option.Option<string>
}

interface User {
  readonly id: number
  readonly username: string
  readonly email: Option.Option<string>
  readonly address: Option.Option<Address>
}

const user: User = {
  id: 1,
  username: "john_doe",
  email: Option.some("john.doe@example.com"),
  address: Option.some({
    city: "New York",
    street: Option.some("123 Main St"),
  }),
}

// Use flatMap to extract the street value
const street = user.address.pipe(Option.flatMap((address) => address.street))

console.log(street)
street // => Option.some("123 Main St")

如果 user.addressSomeOption.flatMap 会应用函数 (address) => address.street 来取出 street 值。

如果 user.addressNone,该函数不会执行,street 保持为 None

这种方式让我们能够简洁地处理嵌套的可选值,避免手动检查,使代码更干净、更易读。

filter

Option.filter 函数允许你根据给定的谓词过滤 Option。如果谓词不满足,或者 OptionNone,结果将是 None

示例(过滤 Option 的值)

下面展示如何用 Option.filter 简化一些代码,写出更符合习惯的写法:

原始代码

import { Option } from "effect"

// Function to remove empty strings from an Option
const removeEmptyString = (input: Option.Option<string>) => {
  if (Option.isSome(input) && input.value === "") {
    return Option.none() // Return None if the value is an empty string
  }
  return input // Otherwise, return the original Option
}

console.log(removeEmptyString(Option.none()))
removeEmptyString(Option.none()) // => Option.none()

console.log(removeEmptyString(Option.some("")))
removeEmptyString(Option.some("")) // => Option.none()

console.log(removeEmptyString(Option.some("a")))
removeEmptyString(Option.some("a")) // => Option.some("a")

重构后的习惯写法

使用 Option.filter,我们可以更简洁地写出同样的逻辑:

import { Option } from "effect"

const removeEmptyString = (input: Option.Option<string>) =>
  Option.filter(input, (value) => value !== "")

console.log(removeEmptyString(Option.none()))
removeEmptyString(Option.none()) // => Option.none()

console.log(removeEmptyString(Option.some("")))
removeEmptyString(Option.some("")) // => Option.none()

console.log(removeEmptyString(Option.some("a")))
removeEmptyString(Option.some("a")) // => Option.some("a")

从 Option 中取值

要从 Option 内部取出存储的值,你可以使用 Option 模块提供的几个辅助函数。下面是可用方法的概览:

getOrThrow

该函数从 Some 中提取值。如果 OptionNone,它会抛出错误。

示例(取出值或抛出错误)

import { Option } from "effect"

console.log(Option.getOrThrow(Option.some(10)))
// Output: 10

console.log(Option.getOrThrow(Option.none()))
// throws: Error: getOrThrow called on a None

getOrNull / getOrUndefined

这些函数把 None 转换为 nullundefined,在与非 Option 风格的代码交互时很有用。

示例(把 None 转换为 nullundefined

import { Option } from "effect"

console.log(Option.getOrNull(Option.some(5)))
Option.getOrNull(Option.some(5)) // => 5

console.log(Option.getOrNull(Option.none()))
Option.getOrNull(Option.none()) // => null

console.log(Option.getOrUndefined(Option.some(5)))
Option.getOrUndefined(Option.some(5)) // => 5

console.log(Option.getOrUndefined(Option.none()))
Option.getOrUndefined(Option.none()) // => undefined

getOrElse

该函数允许你指定当 OptionNone 时返回的默认值。

示例(当 None 时提供默认值)

import { Option } from "effect"

console.log(Option.getOrElse(Option.some(5), () => 0))
Option.getOrElse(Option.some(5), () => 0) // => 5

console.log(Option.getOrElse(Option.none(), () => 0))
Option.getOrElse(Option.none(), () => 0) // => 0

回退

orElse

当一次计算返回 None 时,你可能想尝试另一个会产生 Option 的计算。Option.orElse 函数在这种情况下很有用。它让你能够串联多个计算:如果当前计算得到 None,就继续尝试下一个。这种方式常用于重试逻辑,不断尝试计算,直到有一个成功或所有可能性都用尽。

示例(尝试备选计算)

import { Option } from "effect"

// Simulating a computation that may or may not produce a result
const computation = (): Option.Option<number> =>
  Math.random() < 0.5 ? Option.some(10) : Option.none()

// Simulates an alternative computation
const alternativeComputation = (): Option.Option<number> =>
  Math.random() < 0.5 ? Option.some(20) : Option.none()

// Attempt the first computation, then try an alternative if needed
const program = computation().pipe(
  Option.orElse(() => alternativeComputation()),
)

const result = Option.match(program, {
  onNone: () => "Both computations resulted in None",
  // At least one computation succeeded
  onSome: (value) => `Computed value: ${value}`,
})

console.log(result)
// Output: Computed value: 10

firstSomeOf

你也可以用 Option.firstSomeOf 从一组 Option 值(可迭代对象)中取出第一个 Some 值:

示例(取出第一个 Some 值)

import { Option } from "effect"

const first = Option.firstSomeOf([
  Option.none(),
  Option.some(2),
  Option.none(),
  Option.some(3),
])

console.log(first)
first // => Option.some(2)

与可空类型互操作

处理 Option 数据类型时,你可能会遇到用 undefinednull 表示可选值的代码。Option 模块提供了若干 API,让与这些可空类型的交互变得简单直接。

fromNullable

Option.fromNullishOr 把一个可空值(nullundefined)转换为 Option。如果值是 nullundefined,它返回 Option.none()。否则,它把值包装进 Option.some()

示例(从可空值创建 Option)

import { Option } from "effect"

console.log(Option.fromNullishOr(null))
Option.fromNullishOr(null) // => Option.none()

console.log(Option.fromNullishOr(undefined))
Option.fromNullishOr(undefined) // => Option.none()

console.log(Option.fromNullishOr(1))
Option.fromNullishOr(1) // => Option.some(1)

如果你需要把 Option 转换回可空值,有两个辅助方法:

  • Option.getOrNull:把 None 转换为 null
  • Option.getOrUndefined:把 None 转换为 undefined

与 Effect 互操作

Option 实现了 Yieldable trait,因此可以直接在 Effect.gen 中 yield。若要把 Option 传给 Effect.all 这类 Effect 组合子,请用 Effect.fromOption 显式转换。

Option 如何映射到 Effect

Option 变体映射到的 Effect说明
NoneEffect<never, NoSuchElementError>表示值缺失
Some<A>Effect<A>表示值存在

示例(把 OptionEffect 结合使用)

import { Effect, Option } from "effect"

// Function to get the head of an array, returning Option
const head = <A>(array: ReadonlyArray<A>): Option.Option<A> =>
  array.length > 0 ? Option.some(array[0]!) : Option.none()

head([1, 2, 3]) // => Option.some(1)

// Simulated fetch function that returns Effect
const fetchData = (): Effect.Effect<string, string> => {
  const success = Math.random() > 0.5
  return success
    ? Effect.succeed("some data")
    : Effect.fail("Failed to fetch data")
}

// Option is not an Effect subtype - convert explicitly with Effect.fromOption
const program = Effect.all([Effect.fromOption(head([1, 2, 3])), fetchData()])

Effect.runPromise(program).then(console.log, console.error)
/*
Example Output:
[ 1, 'some data' ]
*/

组合两个或多个 Option

zipWith

Option.zipWith 函数让你用一个给定的函数组合两个 Option 值。它会创建一个新的 Option,其中保存两个原始 Option 值的组合值。

示例(把两个 Option 组合成一个对象)

import { Option } from "effect"

const maybeName: Option.Option<string> = Option.some("John")
const maybeAge: Option.Option<number> = Option.some(25)

// Combine the name and age into a person object
const person = Option.zipWith(maybeName, maybeAge, (name, age) => ({
  name: name.toUpperCase(),
  age,
}))

console.log(person)
person // => Option.some({ name: "JOHN", age: 25 })

如果其中任意一个 Option 值是 None,结果就是 None

示例(处理 None 值)

import { Option } from "effect"

const maybeName: Option.Option<string> = Option.some("John")
const maybeAge: Option.Option<number> = Option.none()

// Since maybeAge is a None, the result will also be None
const person = Option.zipWith(maybeName, maybeAge, (name, age) => ({
  name: name.toUpperCase(),
  age,
}))

console.log(person)
person // => Option.none()

all

若要把多个 Option 值组合起来而不转换它们的内容,可以使用 Option.all。这个函数返回的 Option 结构与输入一致:

  • 如果传入元组,结果就是等长的元组。
  • 如果传入 struct,结果就是包含相同键的 struct。
  • 如果传入 Iterable,结果就是数组。

示例(把多个 Option 组合成元组与 struct)

import { Option } from "effect"

const maybeName: Option.Option<string> = Option.some("John")
const maybeAge: Option.Option<number> = Option.some(25)

//      ┌─── Option<[string, number]>
//      ▼
const tuple = Option.all([maybeName, maybeAge])
console.log(tuple)
tuple // => Option.some(["John", 25])

//      ┌─── Option<{ name: string; age: number; }>
//      ▼
const struct = Option.all({ name: maybeName, age: maybeAge })
console.log(struct)
struct // => Option.some({ name: "John", age: 25 })

如果其中任意一个 Option 值是 None,结果就是 None

示例

import { Option } from "effect"

const maybeName: Option.Option<string> = Option.some("John")
const maybeAge: Option.Option<number> = Option.none()

console.log(Option.all([maybeName, maybeAge]))
Option.all([maybeName, maybeAge]) // => Option.none()

gen

Effect.gen 类似,Option.gen 提供了一种更具可读性的、基于生成器的语法来处理 Option 值,让涉及 Option 的代码更易编写和理解。这种方式与 async/await 类似,但专为 Option 量身定制。

示例(使用 Option.gen 创建一个组合值)

import { Option } from "effect"

const maybeName: Option.Option<string> = Option.some("John")
const maybeAge: Option.Option<number> = Option.some(25)

const person = Option.gen(function* () {
  const name = (yield* maybeName).toUpperCase()
  const age = yield* maybeAge
  return { name, age }
})

console.log(person)
person // => Option.some({ name: "JOHN", age: 25 })

当序列中任意一个 Option 值是 None 时,生成器会立即返回该 None 值,并跳过后续操作:

示例(用 Option.gen 处理 None 值)

在这个示例中,Option.gen 一遇到 None 值就停止执行,从而在不执行后续操作的情况下把缺失值传播出去。

import { Option } from "effect"

const maybeName: Option.Option<string> = Option.none()
const maybeAge: Option.Option<number> = Option.some(25)

const program = Option.gen(function* () {
  console.log("Retrieving name...")
  const name = (yield* maybeName).toUpperCase()
  console.log("Retrieving age...")
  const age = yield* maybeAge
  return { name, age }
})

console.log(program)
/*
Output:
Retrieving name...
*/
program // => Option.none()

这些示例中使用 console.log 仅用于演示目的。使用 Option.gen 时,请避免在生成器函数中引入副作用,因为 Option 应当保持为纯数据结构。

Equivalence

你可以用 Option.makeEquivalence 函数比较 Option 值。该函数允许你为 Option 可能包含的值的类型提供一个 Equivalence,以此指定如何比较 Option 类型的内容。

示例(比较可选数值是否等价)

假设你有一些可选数值,想检查它们是否等价。可以这样使用 Option.makeEquivalence

import { Option, Equivalence } from "effect"

const myEquivalence = Option.makeEquivalence(Equivalence.Number)

console.log(myEquivalence(Option.some(1), Option.some(1)))
// Output: true, both options contain the number 1
myEquivalence(Option.some(1), Option.some(1)) // => true

console.log(myEquivalence(Option.some(1), Option.some(2)))
// Output: false, the numbers are different
myEquivalence(Option.some(1), Option.some(2)) // => false

console.log(myEquivalence(Option.some(1), Option.none()))
// Output: false, one is a number and the other is empty
myEquivalence(Option.some(1), Option.none()) // => false

排序

你可以用 Option.makeOrder 函数对一组 Option 值排序。该函数用于为 Option 中包含的值的类型指定自定义排序规则。

示例(对可选数值排序)

假设你有一个可选数值的列表,想按升序排序,并把空值(Option.none())视为最小:

import { Option, Array, Order } from "effect"

const items = [Option.some(1), Option.none(), Option.some(2)]

// Create an order for sorting Option values containing numbers
const myOrder = Option.makeOrder(Order.Number)

console.log(Array.sort(myOrder)(items))
/*
Output:
[
  { _id: 'Option', _tag: 'None' },           // None appears first because it's considered the lowest
  { _id: 'Option', _tag: 'Some', value: 1 }, // Sorted in ascending order
  { _id: 'Option', _tag: 'Some', value: 2 }
]
*/
Array.sort(myOrder)(items) // => [Option.none(), Option.some(1), Option.some(2)]

示例(按倒序对可选日期排序)

考虑一个更复杂的情形:你有一个对象列表,其中包含可选日期,想按降序排序,并把 Option.none() 值放在末尾:

import { Option, Array, Order } from "effect"

const items = [
  { data: Option.some(new Date(10)) },
  { data: Option.some(new Date(20)) },
  { data: Option.none() },
]

// Define the order to sort dates within Option values in reverse
const sorted = Array.sortWith(
  items,
  (item) => item.data,
  Order.flip(Option.makeOrder(Order.Date)),
)

console.log(sorted)
/*
Output:
[
  { data: { _id: 'Option', _tag: 'Some', value: 1970-01-01T00:00:00.020Z } },
  { data: { _id: 'Option', _tag: 'Some', value: 1970-01-01T00:00:00.010Z } },
  { data: { _id: 'Option', _tag: 'None' } } // None placed last
]
*/
sorted // => [{ data: Option.some(new Date(20)) }, { data: Option.some(new Date(10)) }, { data: Option.none() }]