已发布 上游基线 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)
// Output: { _id: 'Option', _tag: 'Some', value: 1 }

none

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

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

import { Option } from "effect"

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

console.log(noValue)
// Output: { _id: 'Option', _tag: '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()

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

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

import { Option } from "effect"

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

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

为可选属性建模

考虑一个 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(),
}

类型守卫

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

示例(用守卫检查 Option 的值)

import { Option } from "effect"

const foo = Option.some(1)

console.log(Option.isSome(foo))
// Output: 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.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)
// Output: "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))
// Output: { _id: 'Option', _tag: 'Some', value: 2 }

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

示例(对 None 进行映射)

import { Option } from "effect"

// Mapping over None results in None
console.log(Option.map(Option.none(), (n) => n + 1))
// Output: { _id: 'Option', _tag: '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)
// Output: { _id: 'Option', _tag: 'Some', value: '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()))
// Output: { _id: 'Option', _tag: 'None' }

console.log(removeEmptyString(Option.some("")))
// Output: { _id: 'Option', _tag: 'None' }

console.log(removeEmptyString(Option.some("a")))
// Output: { _id: 'Option', _tag: 'Some', value: 'a' }

重构后的习惯写法

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

import { Option } from "effect"

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

console.log(removeEmptyString(Option.none()))
// Output: { _id: 'Option', _tag: 'None' }

console.log(removeEmptyString(Option.some("")))
// Output: { _id: 'Option', _tag: 'None' }

console.log(removeEmptyString(Option.some("a")))
// Output: { _id: 'Option', _tag: 'Some', value: '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)))
// Output: 5

console.log(Option.getOrNull(Option.none()))
// Output: null

console.log(Option.getOrUndefined(Option.some(5)))
// Output: 5

console.log(Option.getOrUndefined(Option.none()))
// Output: undefined

getOrElse

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

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

import { Option } from "effect"

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

console.log(Option.getOrElse(Option.none(), () => 0))
// Output: 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)
// Output: { _id: 'Option', _tag: 'Some', value: 2 }

与可空类型互操作

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

fromNullable

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

示例(从可空值创建 Option)

import { Option } from "effect"

console.log(Option.fromNullable(null))
// Output: { _id: 'Option', _tag: 'None' }

console.log(Option.fromNullable(undefined))
// Output: { _id: 'Option', _tag: 'None' }

console.log(Option.fromNullable(1))
// Output: { _id: 'Option', _tag: 'Some', value: 1 }

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

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

与 Effect 互操作

Option 类型可以作为 Effect 类型的子类型使用,因此你可以把它与 Effect 模块中的函数一起使用。这些函数本是为处理 Effect 值而构建的,但它们同样能正确处理 Option 值。

Option 如何映射到 Effect

Option 变体映射到的 Effect说明
NoneEffect<never, NoSuchElementException>表示值缺失
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()

// 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")
}

// Mixing Either and Effect
const program = Effect.all([head([1, 2, 3]), fetchData()])

Effect.runPromise(program).then(console.log)
/*
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)
/*
Output:
{ _id: 'Option', _tag: 'Some', value: { 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)
// Output: { _id: 'Option', _tag: '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)
/*
Output:
{ _id: 'Option', _tag: 'Some', value: [ 'John', 25 ] }
*/

//      ┌─── Option<{ name: string; age: number; }>
//      ▼
const struct = Option.all({ name: maybeName, age: maybeAge })
console.log(struct)
/*
Output:
{ _id: 'Option', _tag: 'Some', value: { 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]))
// Output: { _id: 'Option', _tag: '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)
/*
Output:
{ _id: 'Option', _tag: 'Some', value: { 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...
{ _id: 'Option', _tag: 'None' }
*/

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

Equivalence

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

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

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

import { Option, Equivalence } from "effect"

const myEquivalence = Option.getEquivalence(Equivalence.number)

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

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

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

排序

你可以使用 Option.getOrder 函数对一组 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.getOrder(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 }
]
*/

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

考虑一个更复杂的情形:你有一个包含可选日期的对象列表,想按降序排序,并把 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.reverse(Option.getOrder(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
]
*/