Skip to content

教程#

所有示例都假定了以下的导入。

方法链:

import * as O from 'optics-ts'

独立:

import * as O from 'optics-ts/standalone'

查看两种语法了解两者之间的区别,以及应该选择哪一种。以下所有的描述和示例都将以两种语法给出。

Lens#

Lens(透镜)是你将要使用的最常见的光学器件。你可以通过调用 optic() 为一个数据结构创建一个光学器件,并通过 prop 将其转换为一个关注对象属性的镜头:

type Data = {
  foo: { bar: number }
  other: string
}
const foo = O.optic<Data>().prop('foo')

foo 现在是一个关注 Data.foo 的镜头。

要深入挖掘,只需再次调用 prop

const bar = O.optic<Data>().prop('foo').prop('bar')
// 或者从我们上面定义的 `foo` 镜头开始
const bar = foo.prop('bar')
// 或者使用 .path() 通过一次调用组合多个 prop 镜头
const bar = O.optic<Data>().path('foo', 'bar')
// 或者使用带有点分隔的字符串路径的 path
const bar = O.optic<Data>().path('foo.bar')

使用 get 通过镜头读取一个值:

const data: Data = {
  foo: { bar: 42 },
  other: 'stuff',
}

O.get(bar)(data)
// 42

使用 setmodify 通过镜头写入关注的值:

O.set(bar)(99)(data)
// {
//   foo: { bar: 99 },
//   other: 'stuff'
// }

O.modify(bar, (x) => x * 100, data)
// {
//   foo: { bar: 4200 },
//   other: 'stuff'
// }

Lens(透镜)是你将要使用的最常见的光学器件。你可以创建一个关注对象属性的镜头:

const foo = O.prop('foo')

foo 现在是一个关注任何给定对象的 foo 属性的镜头。

要深入挖掘,组合多个 prop 镜头:

const bar = O.compose(O.prop('foo'), O.prop('bar'))
// 或者复用我们上面定义的 `foo` 镜头
const bar = O.compose(foo, O.prop('bar'))

因为 prop 是一个经常使用的镜头,你可以直接将字符串参数传递给 compose,它们将被视为 prop 镜头:

const bar = O.compose('foo', 'bar')

使用 get 通过镜头读取一个值:

const data = {
  foo: { bar: 42 },
  other: 'stuff',
}

O.get(bar, data)
// 42

使用 setmodify 通过镜头写入关注的值:

O.set(bar, 99, data)
// {
//   foo: { bar: 99 },
//   other: 'stuff'
// }

O.modify(bar, (x) => x * 100, data)
// {
//   foo: { bar: 4200 },
//   other: 'stuff'
// }

通过光学器件写入总是会创建一个新的数据结构,而不是就地修改现有的数据结构,只复制所需的部分。换句话说,数据是不可变的

Prism#

透镜非常适合聚焦到更大结构的一部分。棱镜很像透镜,但它们不一定匹配任何东西,即它们可以有零焦点。

一个实际的例子是聚焦到联合类型的一个分支。在这里,User.age字段可以是numberundefined。使用optional棱镜,我们只在值为number时聚焦,当它为undefined时不做任何事情:

type User = {
    name: string
    age?: number | undefined
}

const age = O.optic<User>().prop('age').optional()
type User = {
    name: string
    age?: number | undefined
}

const age = O.compose('age', O.optional)

你可以使用preview函数通过棱镜阅读。当棱镜不匹配时,它返回undefined

const userWithAge: User = {
    name: 'Betty',
    age: 42,
}
O.preview(age)(userWithAge)
// 42

const userWithoutAge: User = {
    name: 'Max',
    age: undefined,
}
O.preview(age)(userWithoutAge)
// undefined
const userWithAge: User = {
    name: 'Betty',
    age: 42,
}
O.preview(age, userWithAge)
// 42

const userWithoutAge: User = {
    name: 'Max',
    age: undefined,
}
O.preview(age, userWithoutAge)
// undefined

你可以用setmodify正常地通过棱镜写入。如果棱镜不匹配,值不变:

O.modify(age)((n) => n + 1)(userWithAge)
// {
//   name: 'Betty',
//   age: 43,
// }

O.set(age)(60)(userWithoutAge)
// {
//   name: 'Max',
//   age: undefined,
// }
O.modify(age, (n) => n + 1, userWithAge)
// {
//   name: 'Betty',
//   age: 43,
// }

O.set(age, 60, userWithoutAge)
// {
//   name: 'Max',
//   age: undefined,
// }

guard是创建棱镜的另一种方式。它是optional的泛化,意味着你可以匹配联合类型的任何分支,而不仅仅是非undefined部分:

interface Circle {
    kind: 'circle'
    radius: number
}
interface Rectangle {
    kind: 'rectangle'
    width: number
    height: number
}
type Shape = Circle | Rectangle

function isRectangle(s: Shape): s is Rectangle {
    return s.kind === 'rectangle'
}
const rectWidth = O.optic<Shape>().guard(isRectangle).prop('width')

O.preview(rectWidth)({ kind: 'circle', radius: 10 })
// undefined

O.preview(rectWidth)({ kind: 'rectangle', width: 5, height: 7 })
// 5

O.modify(rectWidth)((w) => w * 2)({ kind: 'rectangle', width: 5, height: 7 })
// { kind: 'rectangle', width: 10, height: 7 })
const rectWidth = O.compose(O.guard(isRectangle), 'width')

O.preview(rectWidth, { kind: 'circle', radius: 10 })
// undefined

O.preview(rectWidth, { kind: 'rectangle', width: 5, height: 7 })
// 5

O.modify(rectWidth, (w) => w * 2, { kind: 'rectangle', width: 5, height: 7 })
// { kind: 'rectangle', width: 10, height: 7 })

注意,上面我们如何将guard棱镜与prop透镜组合。这产生了一个棱镜,所以我们使用preview来通过它阅读。查看组合规则以获取更多信息。

可移除光学器件#

有些光学器件是可移除的。这意味着它们聚焦到容器(例如,数组)的一个元素,并且你可以从容器中删除该元素。

at是一个可移除的棱镜。它聚焦到数组的一个索引,并且也让你可以删除该索引:

interface User {
    name: string
}

const threeUsers: User[] = [
    { name: 'Max' },
    { name: 'Betty' },
    { name: 'Alice' },
]

const secondUser = O.optic<User[]>().at(1)
O.remove(secondUser)(threeUsers)
// [{ name: 'Max' }, { name: 'Alice' }]
interface User {
    name: string
}

const threeUsers: User[] = [
    { name: 'Max' },
    { name: 'Betty' },
    { name: 'Alice' },
]

O.remove(O.at(1), threeUsers)
// [{ name: 'Max' }, { name: 'Alice' }]

如果光学器件不匹配,则移除不会产生任何效果:

const oneUser: User[] = [{ name: 'Max' }]

O.remove(secondUser)(oneUser)
// [{ name: 'Max' }]
const oneUser: User[] = [{ name: 'Max' }]

O.remove(O.at(1), oneUser)
// [{ name: 'Max' }]

遍历#

下一个光学元素类型是遍历。虽然透镜有一个焦点,棱镜有零个或一个焦点(不匹配或匹配),遍历有零个或更多的焦点。

遍历的最简单例子是聚焦到数组的所有元素。要创建这样的遍历,使用elems

type Person {
    name: string
    friends: Person[]
}

const friendsNames = O.optic<Person>()
    .prop('friends')
    .elems()
    .prop('name')
type Person {
    name: string
    friends: Person[]
}

const friendsNames = O.compose('friends', O.elems, 'name')

要通过遍历进行读取,调用collect将所有聚焦的元素收集到一个数组中:

const john = { name: 'John', friends: [] }
const bruce = { name: 'Bruce', friends: [] }
const amy = { name: 'Amy', friends: [john, bruce] }

O.collect(friendsNames)(amy)
// [ 'John', 'Bruce' ]
const john = { name: 'John', friends: [] }
const bruce = { name: 'Bruce', friends: [] }
const amy = { name: 'Amy', friends: [john, bruce] }

O.collect(friendsNames, amy)
// [ 'John', 'Bruce' ]

通过遍历进行写入会写入到所有聚焦的值:

O.modify(friendsNames)((name) => `${name} Wayne`)(amy)
// {
//   name: 'Amy',
//   friends: [
//     { name: 'John Wayne', friends: [] },
//     { name: 'Bruce Wayne', friends: [] },
//   ],
// }
O.modify(friendsNames, (name) => `${name} Wayne`, amy)
// {
//   name: 'Amy',
//   friends: [
//     { name: 'John Wayne', friends: [] },
//     { name: 'Bruce Wayne', friends: [] },
//   ],
// }

再次注意我们如何使用propelemsprop,将透镜与遍历组合,然后再与透镜组合。这产生了一个遍历。查看组合规则以获取更多信息。

有时候,我们需要进一步关注遍历的某些元素。这可以通过将遍历与像when这样的棱镜组合来实现,when会跳过不符合谓词的项:

const even = O.optic<number[]>()
    .elems()
    .when((n) => n % 2 === 0)

O.modify(even)((n) => -n)([1, 2, 3, 4, 5])
// [1, -2, 3, -4, 5]
const even = O.compose(
    O.elems,
    O.when((n: number) => n % 2 === 0)
)

O.modify(even, (n) => -n, [1, 2, 3, 4, 5])
// [1, -2, 3, -4, 5]

多态性#

光学元素可以是多态的,这意味着你可以通过光学元素改变焦点的类型。由于这是一个相对罕见的用例,如果不小心进行可能会造成混淆,因此使用optic_(注意下划线)创建多态光学元素:

type Data = {
    foo: { bar: string }
    other: boolean
}
const bar = O.optic_<Data>().path('foo.bar')

光学元素可以是多态的,这意味着你可以通过光学元素改变焦点的类型。

type Data = {
    foo: { bar: string }
    other: boolean
}
const bar = O.compose('foo', 'bar')

让我们修改bar,使其包含原始字符串的长度:

const data: Data = {
    foo: { bar: 'hello there' },
    other: true,
}

const updated = O.modify(bar)((str) => str.length)(data)
// {
//   foo: { bar: 11 },
//   other: true
// }

这是一个类型安全的操作,即编译器知道updated.foo.bar的类型是number,编辑器自动完成工作正常,等等。

如果你在optics-ts函数的返回值中看到了DisallowedTypeChange类型,那就意味着你试图通过非多态(单态)光学元素改变类型。

const data: Data = {
    foo: { bar: 'hello there' },
    other: true,
}

const updated = O.modify(bar, (str) => str.length, data)
// {
//   foo: { bar: 11 },
//   other: true
// }

这是一个类型安全的操作,即编译器知道updated.foo.bar的类型是number,编辑器自动完成工作正常,等等。