1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-24 04:22:20 +00:00
This commit is contained in:
wizardforcel 2018-05-05 21:24:52 +08:00
parent 2115f76626
commit fd87cc06d3

100
7.md
View File

@ -1,8 +1,8 @@
# 七、项目:机器人 # 七、项目:机器人
> [...] the question of whether Machines Can Think [...] is about as relevant as the question of whether Submarines Can Swim. > [...] 置疑计算机能不能思考 [...] 就相当于置疑潜艇能不能游泳。
> >
> <footer>Edsger Dijkstra, <cite>The Threats to Computing Science</cite></footer> > 艾兹格尔·迪科斯特拉,《计算机科学的威胁》
![Picture of a package-delivery robot](img/chapter_picture_7.jpg) ![Picture of a package-delivery robot](img/chapter_picture_7.jpg)
@ -14,7 +14,7 @@
Meadowfield 村不是很大。 它由 11 个地点和 14 条道路组成。 它可以用`roads`数组来描述: Meadowfield 村不是很大。 它由 11 个地点和 14 条道路组成。 它可以用`roads`数组来描述:
``` ```js
const roads = [ const roads = [
"Alice's House-Bob's House", "Alice's House-Cabin", "Alice's House-Bob's House", "Alice's House-Cabin",
"Alice's House-Post Office", "Bob's House-Town Hall", "Alice's House-Post Office", "Bob's House-Town Hall",
@ -32,7 +32,7 @@ const roads = [
字符串数组并不易于处理。 我们感兴趣的是,我们可以从特定地点到达的目的地。 让我们将道路列表转换为一个数据结构,对于每个地点,都会告诉我们从那里可以到达哪些地点。 字符串数组并不易于处理。 我们感兴趣的是,我们可以从特定地点到达的目的地。 让我们将道路列表转换为一个数据结构,对于每个地点,都会告诉我们从那里可以到达哪些地点。
``` ```js
function buildGraph(edges) { function buildGraph(edges) {
let graph = Object.create(null); let graph = Object.create(null);
function addEdge(from, to) { function addEdge(from, to) {
@ -74,7 +74,7 @@ const roadGraph = buildGraph(roads);
当我们到达新地点时,让我们这样做,在机器人移动时不会改变这种状态,而是在移动之后为当前情况计算一个新状态。 当我们到达新地点时,让我们这样做,在机器人移动时不会改变这种状态,而是在移动之后为当前情况计算一个新状态。
``` ```js
class VillageState { class VillageState {
constructor(place, parcels) { constructor(place, parcels) {
this.place = place; this.place = place;
@ -101,7 +101,7 @@ class VillageState {
包裹对象在移动时不会更改,但会被重新创建。 `move`方法为我们提供新的村庄状态,但完全保留了原有的村庄状态。 包裹对象在移动时不会更改,但会被重新创建。 `move`方法为我们提供新的村庄状态,但完全保留了原有的村庄状态。
``` ```js
let first = new VillageState( let first = new VillageState(
"Post Office", "Post Office",
[{place: "Post Office", address: "Alice's House"}] [{place: "Post Office", address: "Alice's House"}]
@ -124,7 +124,7 @@ console.log(first.place);
在 JavaScript 中,几乎所有的东西都可以改变,所以使用应该持久性的值需要一些限制。 有一个叫做`Object.freeze`的函数,它可以改变一个对象,使其忽略它的属性的写入。 如果你想要小心,你可以使用它来确保你的对象没有改变。 `freeze`确实需要计算机做一些额外的工作,忽略更新可能会让一些人迷惑,让他们做错事。 所以我通常更喜欢告诉人们,不应该弄乱给定的对象,并希望他们记住它。 在 JavaScript 中,几乎所有的东西都可以改变,所以使用应该持久性的值需要一些限制。 有一个叫做`Object.freeze`的函数,它可以改变一个对象,使其忽略它的属性的写入。 如果你想要小心,你可以使用它来确保你的对象没有改变。 `freeze`确实需要计算机做一些额外的工作,忽略更新可能会让一些人迷惑,让他们做错事。 所以我通常更喜欢告诉人们,不应该弄乱给定的对象,并希望他们记住它。
``` ```js
let object = Object.freeze({value: 5}); let object = Object.freeze({value: 5});
object.value = 10; object.value = 10;
console.log(object.value); console.log(object.value);
@ -145,7 +145,7 @@ console.log(object.value);
因为我们希望机器人能够记住东西,以便他们可以制定和执行计划,我们也会传递他们的记忆,并让他们返回一个新的记忆。 因此,机器人返回的东西是一个对象,包含它想要移动的方向,以及下次调用时将返回给它的记忆值。 因为我们希望机器人能够记住东西,以便他们可以制定和执行计划,我们也会传递他们的记忆,并让他们返回一个新的记忆。 因此,机器人返回的东西是一个对象,包含它想要移动的方向,以及下次调用时将返回给它的记忆值。
``` ```js
function runRobot(state, robot, memory) { function runRobot(state, robot, memory) {
for (let turn = 0;; turn++) { for (let turn = 0;; turn++) {
if (state.parcels.length == 0) { if (state.parcels.length == 0) {
@ -166,7 +166,7 @@ function runRobot(state, robot, memory) {
以下是可能的样子: 以下是可能的样子:
``` ```js
function randomPick(array) { function randomPick(array) {
let choice = Math.floor(Math.random() * array.length); let choice = Math.floor(Math.random() * array.length);
return array[choice]; return array[choice];
@ -183,7 +183,7 @@ function randomRobot(state) {
为了使这个复杂的机器人工作,我们首先需要一种方法来创建一些包裹的新状态。 静态方法(通过直接向构造函数添加一个属性来编写)是放置该功能的好地方。 为了使这个复杂的机器人工作,我们首先需要一种方法来创建一些包裹的新状态。 静态方法(通过直接向构造函数添加一个属性来编写)是放置该功能的好地方。
``` ```js
VillageState.random = function(parcelCount = 5) { VillageState.random = function(parcelCount = 5) {
let parcels = []; let parcels = [];
for (let i = 0; i < parcelCount; i++) { for (let i = 0; i < parcelCount; i++) {
@ -202,7 +202,7 @@ VillageState.random = function(parcelCount = 5) {
让我们建立一个虚拟世界。 让我们建立一个虚拟世界。
``` ```js
runRobot(VillageState.random(), randomRobot); runRobot(VillageState.random(), randomRobot);
// → Moved to Marketplace // → Moved to Marketplace
// → Moved to Town Hall // → Moved to Town Hall
@ -212,19 +212,19 @@ runRobot(VillageState.random(), randomRobot);
机器人需要花费很多时间来交付包裹,因为它没有很好规划。 我们很快就会解决。 机器人需要花费很多时间来交付包裹,因为它没有很好规划。 我们很快就会解决。
为了更好地理解模拟,可以使用本章编程环境中提供的`runRobotAnimation`函数。 这将运行模拟,但不是输出文本,而是向你展示机器人在村庄地图上移动。 为了更好地理解模拟,可以使用本章编程环境中提供的`runRobotAnimation`函数。 这将运行模拟,但不是输出文本,而是向你展示机器人在村庄地图上移动。
``` ```js
runRobotAnimation(VillageState.random(), randomRobot); runRobotAnimation(VillageState.random(), randomRobot);
``` ```
`runRobotAnimation`的实现方式现在仍然是一个谜,但是在阅读本书的后面的章节,讨论 Web 浏览器中的 JavaScript 集成之后,将能够猜到它的工作原理。 `runRobotAnimation`的实现方式现在仍然是一个谜,但是在阅读本书的后面的章节,讨论 Web 浏览器中的 JavaScript 集成之后,将能够猜到它的工作原理。
## 邮车的路线 ## 邮车的路线
我们应该能够比随机机器人做得更好。 一个简单的改进就是从现实世界的邮件传递方式中获得提示。 如果我们发现一条经过村庄所有地点的路线,机器人可以通行该路线两次,此时它保证能够完成。 这是一条这样的路线(从邮局开始)。 我们应该能够比随机机器人做得更好。 一个简单的改进就是从现实世界的邮件传递方式中获得提示。 如果我们发现一条经过村庄所有地点的路线,机器人可以通行该路线两次,此时它保证能够完成。 这是一条这样的路线(从邮局开始)。
``` ```js
const mailRoute = [ const mailRoute = [
"Alice's House", "Cabin", "Alice's House", "Bob's House", "Alice's House", "Cabin", "Alice's House", "Bob's House",
"Town Hall", "Daria's House", "Ernie's House", "Town Hall", "Daria's House", "Ernie's House",
@ -235,7 +235,7 @@ const mailRoute = [
为了实现路线跟踪机器人,我们需要利用机器人的记忆。 机器人将其路线的其余部分保存在其记忆中,并且每回合丢弃第一个元素。 为了实现路线跟踪机器人,我们需要利用机器人的记忆。 机器人将其路线的其余部分保存在其记忆中,并且每回合丢弃第一个元素。
``` ```js
function routeRobot(state, memory) { function routeRobot(state, memory) {
if (memory.length == 0) { if (memory.length == 0) {
memory = mailRoute; memory = mailRoute;
@ -246,7 +246,7 @@ function routeRobot(state, memory) {
这个机器人已经快了很多。 它最多需要 26 个回合13 步的路线的两倍),但通常要少一些。 这个机器人已经快了很多。 它最多需要 26 个回合13 步的路线的两倍),但通常要少一些。
``` ```js
runRobotAnimation(VillageState.random(), routeRobot, []); runRobotAnimation(VillageState.random(), routeRobot, []);
``` ```
@ -264,7 +264,7 @@ runRobotAnimation(VillageState.random(), routeRobot, []);
这是一个实现它的函数: 这是一个实现它的函数:
``` ```js
function findRoute(graph, from, to) { function findRoute(graph, from, to) {
let work = [{at: from, route: []}]; let work = [{at: from, route: []}];
for (let i = 0; i < work.length; i++) { for (let i = 0; i < work.length; i++) {
@ -289,7 +289,7 @@ function findRoute(graph, from, to) {
我们的代码无法处理工作列表中没有更多工作项的情况,因为我们知道我们的图是连通的,这意味着可以从其他所有位置访问每个位置。 我们始终能够找到两点之间的路线,并且搜索不会失败。 我们的代码无法处理工作列表中没有更多工作项的情况,因为我们知道我们的图是连通的,这意味着可以从其他所有位置访问每个位置。 我们始终能够找到两点之间的路线,并且搜索不会失败。
``` ```js
function goalOrientedRobot({place, parcels}, route) { function goalOrientedRobot({place, parcels}, route) {
if (route.length == 0) { if (route.length == 0) {
let parcel = parcels[0]; let parcel = parcels[0];
@ -303,28 +303,28 @@ function goalOrientedRobot({place, parcels}, route) {
} }
``` ```
This robot uses its memory value as a list of directions to move in, just like the route-following robot. Whenever that list is empty, it has to figure out what to do next. It takes the first undelivered parcel in the set and, if that parcel hasnt been picked up yet, plots a route towards it. If the parcel _has_ been picked up, it still needs to be delivered, so the robot creates a route towards the delivery address instead. 这个机器人使用它的记忆值作为移动方向的列表,就像寻路机器人一样。 无论什么时候这个列表是空的,它都必须弄清下一步该做什么。 它会取出集合中第一个未送达的包裹,如果该包裹还没有被拾取,则会绘制一条朝向它的路线。 如果包裹已经被拾取,它仍然需要送达,所以机器人会创建一个朝向递送地址的路线。
Lets see how it does. 让我们看看如何实现。
``` ```js
runRobotAnimation(VillageState.random(), runRobotAnimation(VillageState.random(),
goalOrientedRobot, []); goalOrientedRobot, []);
``` ```
This robot usually finishes the task of delivering 5 parcels in around 16 turns. Slightly better than `routeRobot`, but still definitely not optimal. 这个机器人通常在大约 16 个回合中,完成了送达 5 个包裹的任务。 略好于`routeRobot`,但仍然绝对不是最优的。
## Exercises ## 练习
### Measuring a robot ### 测量机器人
Its hard to objectively compare robots by just letting them solve a few scenarios. Maybe one robot just happened to get easier tasks, or the kind of tasks that it is good at, whereas the other didnt. 很难通过让机器人解决一些场景来客观比较他们。 也许一个机器人碰巧得到了更简单的任务,或者它擅长的那种任务,而另一个没有。
Write a function `compareRobots` that takes two robots (and their starting memory). It should generate 100 tasks and let each of the robots solve each of these tasks. When done, it should output the average number of steps each robot took per task. 编写一个`compareRobots`,接受两个机器人(和它们的起始记忆)。 它应该生成 100 个任务,并让每个机器人解决每个这些任务。 完成后,它应输出每个机器人每个任务的平均步数。
For the sake of fairness, make sure that you give each task to both robots, rather than generating different tasks per robot. 为了公平起见,请确保你将每个任务分配给两个机器人,而不是为每个机器人生成不同的任务。
``` ```js
function compareRobots(robot1, memory1, robot2, memory2) { function compareRobots(robot1, memory1, robot2, memory2) {
// Your code here // Your code here
} }
@ -332,41 +332,33 @@ function compareRobots(robot1, memory1, robot2, memory2) {
compareRobots(routeRobot, [], goalOrientedRobot, []); compareRobots(routeRobot, [], goalOrientedRobot, []);
``` ```
Youll have to write a variant of the `runRobot` function that, instead of logging the events to the console, returns the number of steps the robot took to complete the task. ### 机器人的效率
Your measurement function can then, in a loop, generate new states and count the steps each of the robots takes. When it has generated enough measurements, it can use `console.log` to output the average for each robot, which is the total amount of steps taken divided by the number of measurements. 你能写一个机器人,比`goalOrientedRobot`更快完成递送任务吗? 如果你观察机器人的行为,它会做什么明显愚蠢的事情?如何改进它们?
### Robot efficiency 如果你解决了上一个练习,你可能打算使用`compareRobots`函数来验证你是否改进了机器人。
Can you write a robot that finishes the delivery task faster than `goalOrientedRobot`? If you observe that robots behavior, what obviously stupid things does it do? How could those be improved? ```js
If you solved the previous exercise, you might want to use your `compareRobots` function to verify whether you improved the robot.
```
// Your code here // Your code here
runRobotAnimation(VillageState.random(), yourRobot, memory); runRobotAnimation(VillageState.random(), yourRobot, memory);
``` ```
The main limitation of `goalOrientedRobot` is that it considers only one parcel at a time. It will often walk back and forth across the village because the parcel it happens to be looking at happens to be at the other side of the map, even if there are others much closer. ### 持久性分组
One possible solution would be to compute routes for all packages, and then take the shortest one. Even better results can be obtained, if there are multiple shortest routes, by preferring the ones that go to pick up a package instead of delivering a package. 标准 JavaScript 环境中提供的大多数数据结构不太适合持久使用。 数组有`slice``concat`方法,可以让我们轻松创建新的数组而不会损坏旧数组。 但是`Set`没有添加或删除项目并创建新集合的方法。
### Persistent group 编写一个新的类`PGroup`,类似于第六章中的`Group`类,它存储一组值。 像`Group`一样,它具有`add``delete``has`方法。
Most data structures provided in a standard JavaScript environment arent very well suited for persistent use. Arrays have `slice` and `concat` methods, which allow us to easily create new arrays without damaging the old one. But `Set`, for example, has no methods for creating a new set with an item added or removed. 然而,它的`add`方法应该返回一个新的`PGroup`实例,并添加给定的成员,并保持旧的不变。 与之类似,`delete`创建一个没有给定成员的新实例。
Write a new class `PGroup`, similar to the `Group` class from [Chapter 6](06_object.html#groups), which stores a set of values. Like `Group`, it has `add`, `delete`, and `has` methods. 该类应该适用于任何类型的值,而不仅仅是字符串。 当与大量值一起使用时,它不一定非常高效。
Its `add` method, however, should return a _new_ `PGroup` instance with the given member added, and leave the old one unchanged. Similarly, `delete` creates a new instance without a given member. 构造函数不应该是类接口的一部分(尽管你绝对会打算在内部使用它)。 相反,有一个空的实例`PGroup.empty`,可用作起始值。
The class should work for values of any type, not just strings. It does _not_ have to be efficient when used with large amounts of values. 为什么只需要一个`PGroup.empty`值,而不是每次都创建一个新的空分组?
The constructor shouldnt be part of the class interface (though youll definitely want to use it internally). Instead, there is an empty instance, `PGroup.empty`, that can be used as a starting value. ```js
Why do you need only one `PGroup.empty` value, rather than having a function that creates a new, empty map every time?
```
class PGroup { class PGroup {
// Your code here // Your code here
} }
@ -382,13 +374,3 @@ console.log(a.has("b"));
console.log(b.has("a")); console.log(b.has("a"));
// → false // → false
``` ```
The most convenient way to represent the set of member values is still an array, since those are easy to copy.
When a value is added to the group, you can create a new group with a copy of the original array that has the value added (for example, using `concat`). When a value is deleted, you filter it from the array.
The class constructor can take such an array as argument, and store it as the instances (only) property. This array is never updated.
To add a property (`empty`) to a constructor that is not a method, you have to add it to the constructor after the class definition, as a regular property.
You need only one `empty` instance because all empty groups are the same and instances of the class dont change. You can create many different groups from that single empty group without affecting it.