4.3 结构共享
正如我们在上一节中提到的,结构化共享允许高效地创建不可变数据的新版本。
在DO中,我们在变异的计算阶段利用结构共享来基于系统的当前状态来计算系统的下一个状态。在计算阶段,我们不必处理状态管理:这会延迟到提交阶段。
因此,变异计算阶段涉及的代码是无状态的,与查询代码一样简单。
你:我真的对这种创建新版本数据的高效方式很感兴趣。它怎麽工作?
乔:让我们从我们的图书馆系统举一个简单的例子。假设您想要修改目录中某本书中某个字段的值,例如“守望者”的出版年份。你能告诉我守望者出版年的信息路径是什么吗?
在快速浏览了图4.2中的目录数据之后,您回答:
![](https://i-blog.csdnimg.cn/blog_migrate/2a208eeb10c57e08aa17c95460773107.png)
你:“守望者”出版年的信息路径是:[“catalog”, “booksByIsbn”, “978-1779501127”, “publicationYear”].
乔:现在,让我演示如何使用Lodash提供的不可变函数_.set()。
你:你说的不可变函数是什么意思?当我查看_.set()的Lodash文档时,它说它改变了对象。
乔:你说得对。默认情况下,Lodash函数不是不可变的。为了使用不变版本的函数,我们需要使用Lodash FP模块(函数编程),如Lodash FP指南中所述。
您:不可变函数是否与可变函数具有相同的签名?
乔:默认情况下,不可变函数中的参数顺序是随机排列的。在Lodash FP指南中,他们解释了如何解决这个问题:使用清单4.1中的这段代码,不可变函数的签名与可变函数的签名完全相同。
清单4.1 配置Lodash,使不可变函数与可变函数具有相同的签名
_ = fp.convert({
"cap": false,
"curry": false,
"fixed": false,
"immutable": true,
"rearg": false
});
TIP 为了使用Lodash不可变函数,我们使用了Lodash FP模块,并对其进行了配置,使不可变函数的签名与Lodash文档网站中的签名相同。
您:所以基本上,在使用函数的不可变版本时,我仍然可以依赖Lodash文档。
乔:除了文档中说函数会改变对象的那一段。
你:当然!
乔:现在,让我向您展示如何使用Lodash提供的不可变函数_.set()编写代码来创建库数据的一个版本。
清单4.2 创建Watchman发布年份为1986的库的版本
var nextLibrary = _.set(
library,
["catalog", "booksByIsbn", "978-1779501127","publicationYear"],
1986);
NOTE 当一个函数不是改变数据,而是在不改变它接收的数据的情况下创建数据的新版本时,它被称为不可变的。
你:你之前告诉过我,结构共享使得不可变函数在内存和计算方面变得高效。你能告诉我是什么让它们变得高效吗?
乔:很乐意。但在此之前,你必须回答一系列问题。准备好了吗?
你:是的。
乔:图书馆数据的哪一部分会受到更新《守望者》出版物年份的影响:用户管理还是Catalog?
你:只有Catalog。
乔:Catalog的哪一部分?
你:只有booksByIsbn索引。
乔:booksByIsbn索引的哪一部分?
你:只有保存守望者信息的Book record。
乔:Book record的哪一部分?
你:只有PublicationYear字段。
乔:当您使用一个不可变的函数创建一个新版本的Library时,其中Watchman的发布年份设置为1986(而不是1987),它会创建一个新的Library hash map,该map递归地使用两个版本之间当前Library的公共部分,而不是深入复制它们。这种技术被称为:结构共享(structural sharing)。
你:你能给我描述一下结构共享是如何一步一步地运作的吗?
乔抓起一张纸,画出了图4.3中说明结构共享的图。
![](https://i-blog.csdnimg.cn/blog_migrate/ee107d5dd61b4ba3501d35e09490e356.png)
乔:下一个版本的库,使用与旧版本相同的UserManagement散列映射。下一个库中的Catalog使用与当前Catalog相同的AuthsById。Next Catalog中的Watchman Book记录使用当前Book的所有字段,但PublationYear字段除外。
TIP 结构共享提供了一种高效的方式(内存和计算),通过递归共享不需要更改的部分来创建新版本的数据。
你:那太酷了!
乔:的确。现在,让我向您展示如何使用不可变函数编写用于添加会员的变体。图4.4显示了一个图表,它说明了当我们添加一个会员时,结构共享是什么样子。
![](https://i-blog.csdnimg.cn/blog_migrate/bd9c7606d9f3ef0e5c91c4f04a94d08f.png)
你:catalog和librarians的Hash Map不需要复制,这太酷了!
乔:在代码方面,我们必须编写一个Library.addMember()函数来委托给UserManagement.addMember()。
您:我猜它将类似于我们在第2章中为实现搜索图书查询而编写的代码,其中Library.searchBooksByTitleJSON()委托给Catalog.searchBooksByTitle()。
乔:相似之处在于,所有函数都是静态的,它们接收作为参数操作的数据。但有两点不同:首先,突变可能会失败,例如,如果要添加的会员已经存在。其次,Library.addMember()的代码比Library.searchBooksByTitleJSON()的代码稍微复杂一些,因为我们必须创建引用新版本UserManagement的库的新版本。清单4.3显示了添加会员的突变的代码。
清单4.3 添加成员的突变的代码
UserManagement.addMember = function(userManagement, member) {
var email = _.get(member, "email");
var infoPath = ["membersByEmail", email];
if (_.has(userManagement, infoPath)) { //1
throw "Member already exists.";
}
var nextUserManagement = _.set(userManagement, //2
infoPath, member);
return nextUserManagement;
}
Library.addMember = function(library, member) {
var currentUserManagement = _.get(library, "userManagement");
var nextUserManagement = UserManagement.addMember(currentUserManagement, member);
var nextLibrary = _.set(library, "userManagement", nextUserManagement); //3
return nextLibrary;
}
- 检查是否已存在具有相同电子邮件地址的会员
- 创建包含该会员的新版本的userManagement
- 创建包含新版本的userManagement的库的新版本
你:我觉得有点奇怪,不变的函数返回数据的更新版本,而不是原地更改它。
乔:10年前,当我第一次在Clojure中遇到不可变数据时,对我来说也很奇怪。
你:你花了多长时间才习惯?
乔:几个星期。