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.none 和 Option.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>
}
可选性只作用于属性的值。键 "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.isSome 和 Option.isNone 这两个 guard 检查一个 Option 是 Some 还是 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 的两种情况:分别为 None 和 Some 指定独立的回调。
示例(对 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),就应用该转换函数。如果 Option 是 None,则忽略该函数,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.flatMap 从 address 中提取 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.address 是 Some,Option.flatMap 会应用函数 (address) => address.street 来取出 street 值。
如果 user.address 是 None,该函数不会执行,street 保持为 None。
这种方式让我们能够简洁地处理嵌套的可选值,避免手动检查,使代码更干净、更易读。
filter
Option.filter 函数允许你根据给定的谓词过滤 Option。如果谓词不满足,或者 Option 是 None,结果将是 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 中提取值。如果 Option 是 None,它会抛出错误。
示例(取出值或抛出错误)
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 转换为 null 或 undefined,在与非 Option 风格的代码交互时很有用。
示例(把 None 转换为 null 或 undefined)
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
该函数允许你指定当 Option 为 None 时返回的默认值。
示例(当 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 数据类型时,你可能会遇到用 undefined 或 null 表示可选值的代码。Option 模块提供了若干 API,让与这些可空类型的交互变得简单直接。
fromNullable
Option.fromNullishOr 把一个可空值(null 或 undefined)转换为 Option。如果值是 null 或 undefined,它返回 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 | 说明 |
|---|---|---|
None | Effect<never, NoSuchElementError> | 表示值缺失 |
Some<A> | Effect<A> | 表示值存在 |
示例(把 Option 与 Effect 结合使用)
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() }]