轨道
/
Roc
Roc
/
练习
/
透镜人
透镜人

透镜人

中等

说明

使用 lens 更新嵌套记录(仅适用于具有不可变数据的语言)。

更新嵌套的不可变记录的字段有点烦人。 结构越深,这类情况的代码就越繁琐。 比如,你有一个 Person,它包含一个 Address,Address 里有一个 Street,Street 里有一个 Number,那么要更新 Number,就得先创建一个带有新 Number 的 Street,再创建一个带有新 Street 的 Address,最后创建一个带有新 Address 的 Person。 是不是已经晕了?

解决这个问题的一个办法是使用 lens。

用 lens 实现几个访问记录的函数。 测试套件也允许你完全不用 lens,这样你就可以尝试不同的思路。

在 Roc 中,lens 用得不多,因为记录更新语法已经能很好地覆盖大多数使用场景。例如,看看这个嵌套记录:

bob = { hair: { color: Brown, length: Long }, mouth: Smiling, height: 6.0 }

现在给 Bob 换个发型:

new_bob = { ..bob, hair: { ..bob.hair, length: Short } }

完全不需要 lens!话虽如此,尽管 lens 在 Roc 中很少见,但在 Haskell 和 Scala 等其他语言里却很常见,所以你可能会好奇:在 Roc 中要怎么实现 lens 呢?这也是练习 Roc 一些高级特性的好方法,比如参数化类型,以及包含函数的记录。事实上,下面就是在 Roc 中表示 lens 的一种方式:

Lens(whole, part) := {
	get : whole -> part,
	set : whole, part -> whole,
}

这是一个参数化类型:whole 和 part 可以是任意类型。whole 通常是记录,而 part 可以是任意字段类型。get 字段包含一个函数,它返回给定 whole 中某个特定的 part;而 set 字段包含一个函数,它返回给定 whole 的一个副本,只是其中某个特定的 part 被替换成了给定的 part。

例如,下面这个 lens 可以获取或设置一个人的头发:

hair_lens = {
	get: |person| person.hair,
	set: |person, hair| { ..person, hair },
}

我们可以用这个 lens 聚焦到 Bob 的头发,像这样:

bob_hair = bob |> hair_lens.get

也可以这样改变他的头发:

new_hair = { ..bob.hair, length: Short }
new_bob = bob |> (hair_lens.set)(new_hair)

注意:hair_lens.set 外面的括号是必需的,没有它们,Roc 会在 hair_lens 的类型中寻找一个名为 set 的函数。

当你开始组合 lens 时,它们就变得特别有用,让你能够获取和设置深层嵌套的部分。这可以通过实现下面这样的函数来做到:

compose : Lens(outer, inner), Lens(inner, part) -> Lens(outer, part)

实现这个函数之后,你就可以用它创建像这样的发长 lens:

length_lens = {
	get: |hair| hair.length,
	set: |hair, length| { ..hair, length },
}
hair_length_lens = compose(hair_lens, length_lens)

现在你可以再给 Bob 换个发型:

set_hair_length = hair_length_lens.set
new_bob = bob |> set_hair_length(Short)

你可能还想实现一个函数,用来对嵌套的部分应用任意变换:

over : Lens(whole, part), whole, (part -> part) -> whole

实现之后,你可以这样使用 over:

cut_hair = |current_length| match current_length {
	Long => Short
	Short => Bald
	Bald => Bald
}
new_bob = hair_length_lens |> over(bob, cut_hair)

在这个练习中,测试会检查你的代码能否修改下面这个嵌套记录中的各个嵌套部分:

test_person : Person
test_person = {
	name: { forenames: "Jane Joanna", surname: "Doe" },
	birth: {
		born_at: { street: "Longway", house_number: 1024, place: "Springfield", country: "United States" },
		born_on: { year: 1984, month: 4, day: 12 },
	},
	address: { street: "Shortlane", house_number: 2, place: "Fallmeadow", country: "Canada" },
}

我们建议你先用 Roc 的记录更新语法通过测试,然后再试着改用 lens。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Roc Exercism

准备好开始 透镜人 了吗?

注册 Exercism,借助 125 个练习 和真人导师指导,学习并掌握 Roc,全部免费。