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.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()
示例(用 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>
}
可选性只作用于属性的值。键 "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.isSome 和 Option.isNone 这两个守卫检查一个 Option 是 Some 还是 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 的两种情况:分别为 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)
// Output: "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))
// 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.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)
// Output: { _id: 'Option', _tag: 'Some', value: '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()))
// 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 中提取值。如果 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)))
// 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
该函数允许你指定当 Option 为 None 时返回的默认值。
示例(当 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 数据类型时,你可能会遇到用 undefined 或 null 表示可选值的代码。Option 模块提供了若干 API,让与这些可空类型的交互变得简单直接。
fromNullable
Option.fromNullable 把一个可空值(null 或 undefined)转换为 Option。如果值是 null 或 undefined,它返回 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 | 说明 |
|---|---|---|
None | Effect<never, NoSuchElementException> | 表示值缺失 |
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()
// 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
]
*/