所以您听说过不变性和所有的甜蜜,但实际上是什么?
我们将在本文中使用TypeScript
,但相同的原理适用于不同的语言。
简单的单词不变性意味着一个价值永远无法更改,或者从更深层次的意义上讲,值得一提的值永远无法更改。
当您处理不变的值时,您总是可以安全地“捕获”它们,因为您知道它们永远不会改变,使用可变值也需要制作副本。
让我们看看一些不变的代码的用法:
let arr: ReadonlyArray<number> = []
while (arr.length < 10) {
const newArr = [...arr, arr.length]
setTimeout(() => {
console.log(newArr.length)
}, 0)
arr = newArr
}
执行该代码将在控制台上每一个1到10之间的每个数字(包括)。
。如果我们要对可变代码做同样的事情,我们会取得完全不同的结果,让我们看看:
const arr: Array<number> = []
while (arr.length < 10) {
arr.push(arr.length)
setTimeout(() => {
console.log(arr.length)
}, 0)
}
此代码将打印10倍10倍。
我们在这里所做的是展示“捕获”的含义,对setTimeout
的呼叫从本地范围中获取引用,并且它将其运送到其他范围,setTimeout
将在我们程序中的所有同步操作后始终执行完成。
在第一个示例中,在第一个示例中,我们有效地获得了10个不同长度的不同阵列,而在第二个示例中,我们得到一个包含从0到9的所有数字(长度为10)的单个数组。
复制写
我们在上一个示例中使用的技术通常被称为“写入”,这意味着每当我们打算“突变”某些内容时,我们创建了它的新副本,这允许初始结构(预先享受)保持不变(又名冷冻)。
我们可以看到,使用Object.freeze
不会在任何时候引起错误。
let arr: ReadonlyArray<number> = Object.freeze([])
while (arr.length < 10) {
const newArr = Object.freeze([...arr, arr.length])
setTimeout(() => {
console.log(newArr.length)
}, 0)
arr = newArr
}
相同的技术可以应用于对象,通常可以“克隆”任何结构。
一切都很好吗?好吧,除了表现性能之外,随着事物的增长,克隆变得越来越昂贵。
const copyOnWrite = (n: number) => {
console.time(`copy-on-write-${n}`)
let arr: ReadonlyArray<number> = []
while (arr.length < n) {
const newArr = [...arr, arr.length]
arr = newArr
}
console.timeEnd(`copy-on-write-${n}`)
}
copyOnWrite(10)
copyOnWrite(100)
copyOnWrite(1000)
copyOnWrite(10000)
copyOnWrite(100000)
此程序将产生:
copy-on-write-10: 0.074ms
copy-on-write-100: 0.093ms
copy-on-write-1000: 1.034ms
copy-on-write-10000: 82.26ms
copy-on-write-100000: 52.010s
显然,这变得非常迅速,尤其是在无限尺寸的问题(您不知道需要克隆的结构的大小)中。
)。那么,不变性搞砸了吗?否...但是在写作上复制。
持续的数据结构
我不会涉及持续数据结构的所有细节和技术,如果您对这个主题感兴趣,我建议您遵循Erik Demaine的惊人课程,标题为“高级数据结构”,免费,来自MIT OpenCourse软件平台:https://ocw.mit.edu/courses/6-851-advanced-data-structures-spring-2012/。
为了本文,我们会说持续的数据结构是记住其历史记录的数据结构,并且每个新的“复制”与原件尽可能多地共享。
如果在持续数组结构之前看到的示例应记住并与前一个元素共享所有元素,而不必每次克隆所有内容。
让我们使用持续数组进行相同的示例,我们将使用基于以下论文的新@fp-ts/data
中的“块”数据结构:http://aleksandar-prokopec.com/resources/docs/lcpc-conc-trees.pdf。
import * as Chunk from "@fp-ts/data/Chunk"
const copyOnWrite = (n: number) => {
console.time(`persistent-${n}`)
let arr: Chunk.Chunk<number> = Chunk.empty()
while (arr.length < n) {
const newArr = Chunk.append(Chunk.size(arr))(arr)
arr = newArr
}
console.timeEnd(`persistent-${n}`)
}
copyOnWrite(10)
copyOnWrite(100)
copyOnWrite(1000)
copyOnWrite(10000)
copyOnWrite(100000)
我们可以看到代码看起来几乎与上一个相同,但执行甚至没有比较:
persistent-10: 0.534ms
persistent-100: 0.728ms
persistent-1000: 1.493ms
persistent-10000: 14.425ms
persistent-100000: 10.705ms
100000
甚至比10000
快,这似乎令人惊讶,这是因为V8
(Node.js
背后的JS引擎)能够优化代码。
关键是,复杂性不会随大小而增长,并且所有先前的参考都可以安全地捕获,因为它们仍然有效。
魔术?有时感觉就像。
结论
如果您喜欢这种事情,请不要忘记查看https://github.com/fp-ts和https://github.com/effect-TS中正在进行的所有工作。而且,如果可以的话,请捐赠一些$$
来支持贡献者的惊人工作。
推论
为什么记录和元组提案https://github.com/tc39/proposal-record-tuple不好?
它将不变性与写入和结构冻结上的复制相结合,并且实现不允许将自定义类型混合在一起,因此当使用时,它是全in。
总的来说,如果被接受,我觉得这将减慢适当的不变数据结构的采用,以接近不可逆转的方式,功能社区将忽略不变性的目标,而不变性的概念到来来自。
什么会更好?
您可以从本文中看到,已经有可能完成我们需要做的一切可以说是一个不错的。
的一个例子是操作员超载提案https://github.com/tc39/proposal-operator-overloading将允许重叠的===
,并且还将在一枪中解决许多其他问题(例如Numeric类型扩展,可管道操作员以及提案中列出的所有问题)。<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< /p>
除了可以添加自定义的#
操作员,以创建对象和数组,并使用超载===
的原型创建对象和数组。