Skip to content

Commit 529018d

Browse files
committed
Replace list class exercises with group class
Issue marijnh#316
1 parent e996408 commit 529018d

11 files changed

Lines changed: 255 additions & 230 deletions

06_object.md

Lines changed: 79 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,76 +1063,110 @@ JavaScript.
10631063

10641064
hint}}
10651065

1066-
### List class
1066+
### Groups
10671067

1068-
{{index "list (exercise)", interface, iterator, "static method", "List class"}}
1068+
{{index "groups (exercise)", "Set class", "Group class", "set (data structure)"}}
10691069

1070-
Rewrite the list data structure from the exercises in [Chapter
1071-
?](data#list) as a ((class)). Give `List` objects their old
1072-
`value` and `rest` properties, but also a `toArray` method and a
1073-
`length` getter that returns the length of the list. Make `fromArray`
1074-
a static method on the `List` constructor.
1070+
{{id groups}}
10751071

1076-
In order for lists to work as a class with methods, we can no longer
1077-
represent the empty list as `null`, but have to create a special
1078-
instance of our class that acts as the empty list placeholder, and
1079-
compare with that instance, instead of `null`, when checking if we've
1080-
reached the end of a list. Store this instance in `List.empty` (a
1081-
static property).
1072+
The standard JavaScript environment provides another data structure
1073+
called `Set`. Like an instance of `Map`, a set holds a collection of
1074+
values. Unlike `Map`, it does not associate other values with those—it
1075+
just tracks which values are part of the set. A value can only be part
1076+
of a set once—adding it again doesn't have any effect.
1077+
1078+
{{index "add method", "delete method", "has method"}}
1079+
1080+
Write a class called `Group` (since `Set` is already taken). Like
1081+
`Set`, it has `add`, `delete`, and `has` methods. Its constructor
1082+
creates an empty group, `add` adds a value to the group (but only if
1083+
it isn't already a member), `delete` removes its argument from the
1084+
group (it if was a member), and `has` returns a Boolean value
1085+
indicating whether its argument is a member of the group.
1086+
1087+
{{index "=== operator", "indexOf method"}}
1088+
1089+
Use the `===` operator, or something equivalent such as `indexOf`, to
1090+
determine whether two values are the same.
1091+
1092+
{{index "static method"}}
1093+
1094+
Give the class a static `from` method that takes an iteratable object
1095+
as argument and creates a group that contains all the values produced
1096+
by iterating over it.
10821097

10831098
{{if interactive
10841099

10851100
```{test: no}
1086-
class List {
1101+
class Group {
10871102
// Your code here.
10881103
}
10891104
1090-
console.log(List.fromArray([10, 20]));
1091-
// → {value: 10, rest: {value: 20, rest: null}}
1092-
console.log(List.fromArray([10, 20, 30]).toArray());
1093-
// → [10, 20, 30]
1094-
console.log(new List(2, List.empty).length);
1095-
// → 1
1105+
let group = Group.from([10, 20]);
1106+
console.log(group.has(10));
1107+
// → true
1108+
console.log(group.has(30));
1109+
// → false
1110+
group.add(10);
1111+
group.delete(10);
1112+
console.log(group.has(10));
1113+
// → false
10961114
```
10971115

10981116
if}}
10991117

11001118
{{hint
11011119

1102-
{{index "list (exercise)"}}
1120+
{{index "groups (exercise)", "Group class", "indexOf method", "includes method"}}
1121+
1122+
The easiest way to do this is to store an ((array)) of group members
1123+
in an instance property. The `includes` or `indexOf` methods can be
1124+
used to check whether a given value is in the array.
1125+
1126+
{{index "push method"}}
11031127

1104-
Your class' constructor should take a value and a rest list as
1105-
parameters, and store those in the instance.
1128+
Your class' ((constructor)) can set the member collection to an empty
1129+
array. When `add` is called, it must check whether the given value is
1130+
in the array, and add it, for example with `push`, otherwise.
11061131

1107-
The static `fromArray` method can be written inside the class
1108-
declaration. The `empty` property, because it is not a function, must
1109-
be added to the prototype afterwards—but that would have been
1110-
necessary anyway, because we couldn't create an instance of `List`
1111-
before we've finished defining the class.
1132+
{{index "filter method"}}
11121133

1113-
The value of the empty list object is not important—when using lists
1114-
correctly, it should not be read. The `toArray` and `length` methods
1115-
do the right thing automatically, even on this object, if you make the
1116-
end conditions of their loops check for `== List.empty`.
1134+
Deleting an element from an array, in `delete`, is slightly awkward,
1135+
but you can use `filter` to create a new array without the value.
1136+
Don't forget to overwrite the property holding the members with the
1137+
newly filtered version of the array.
1138+
1139+
{{index "for/of loop", "iterable interface"}}
1140+
1141+
The `from` method can use a `for`/`of` loop to get the values out of
1142+
the iterable object, and call `add` to put them into a newly created
1143+
group.
11171144

11181145
hint}}
11191146

1120-
### List iteration
1147+
### Iterable groups
11211148

1122-
{{index "list (exercise)", interface, "iterator interface"}}
1149+
{{index "groups (exercise)", interface, "iterator interface", "Group class"}}
11231150

1124-
{{id list_iterator}}
1151+
{{id group_iterator}}
11251152

1126-
Make the `List` class from the previous exercise iterable. Refer back
1153+
Make the `Group` class from the previous exercise iterable. Refer back
11271154
to the section about the iterator interface earlier in the chapter if
11281155
you aren't clear on the exact form of the interface anymore.
11291156

1157+
If you used an array to represent the group's members, don't just
1158+
return the iterator created by calling the `Symbol.iterator` method on
1159+
the array. That would work, but defeats the purpose of this exercise.
1160+
1161+
It is okay if your iterator behaves strangely when the group is
1162+
modified during iteration.
1163+
11301164
{{if interactive
11311165

11321166
```{test: no}
11331167
// Your code here (and the code from the previous exercise)
11341168
1135-
for (let value of List.fromArray(["a", "b", "c"])) {
1169+
for (let value of Group.from(["a", "b", "c"])) {
11361170
console.log(value);
11371171
}
11381172
// → a
@@ -1144,16 +1178,16 @@ if}}
11441178

11451179
{{hint
11461180

1147-
{{index "list (exercise)"}}
1181+
{{index "groups (exercise)", "Group class", "next method"}}
11481182

1149-
It is probably worthwhile to define a new class `ListIterator`.
1150-
Iterator instances should have a property that tracks the current rest
1151-
of the list. If that's the empty list, the `next` method can say it is
1152-
done. If not, it should return the current element and move the
1153-
current rest forward.
1183+
It is probably worthwhile to define a new class `GroupIterator`.
1184+
Iterator instances should have a property that tracks the current
1185+
position in the group. Every time `next` is called, it checks whether
1186+
it is done, and if not, moves past the current value and returns it.
11541187

1155-
The list class itself gets a method named by `Symbol.iterator` which,
1156-
when called, returns a new instance of the list iterator class.
1188+
The `Group` class itself gets a method named by `Symbol.iterator`
1189+
which, when called, returns a new instance of the iterator class for
1190+
that group.
11571191

11581192
hint}}
11591193

07_robot.md

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -621,50 +621,50 @@ pick up a package, instead of delivering a package.
621621

622622
hint}}
623623

624-
### Persistent map
624+
### Persistent group
625625

626-
{{index "persistent map (exercise)", "persistent data structure", "Map class", "map (data structure)", "PMap class"}}
626+
{{index "persistent group (exercise)", "persistent data structure", "Set class", "set (data structure)", "Group class", "PGroup class"}}
627627

628628
Most data structures provided in a standard JavaScript environment
629629
aren't very well suited for persistent use. Arrays have `slice` and
630630
`concat` methods, which allow us to easily create new arrays without
631-
damaging the old one. But `Map`, for example, has no methods for
632-
creating a new map with an item added or removed.
631+
damaging the old one. But `Set`, for example, has no methods for
632+
creating a new set with an item added or removed.
633633

634-
Write a new class `PMap`, which stores a mapping from keys to other
635-
values, and, like `Map`, has a `get` method to retrieve a value and a
636-
`has` method to check if a value is present.
634+
Write a new class `PGroup`, similar to the `Group` class from [Chapter
635+
?](object#groups), which stores a set of values. Like `Group`, it has
636+
`add`, `delete`, and `has` methods.
637637

638-
Its `set` method, however, should return a _new_ `PMap` instance with
639-
the given key added, and leave the old one unchanged. Similarly,
640-
`delete` creates a new instance without a key.
638+
Its `add` method, however, should return a _new_ `PGroup` instance
639+
with the given member added, and leave the old one unchanged.
640+
Similarly, `delete` creates a new instance without a member.
641641

642642
The class should work for keys of any type, not just strings. It does
643643
_not_ have to be efficient when used with large amounts of keys.
644644

645645
The ((constructor)) shouldn't be part of the class' ((interface))
646646
(though you'll definitely want to use it internally). Instead, there
647-
is an empty instance, `PMap.empty`, that can be used as a starting
647+
is an empty instance, `PGroup.empty`, that can be used as a starting
648648
value.
649649

650650
{{index singleton}}
651651

652-
Why do you only need one `PMap.empty` value, rather than having a
652+
Why do you only need one `PGroup.empty` value, rather than having a
653653
function that creates a new, empty map every time?
654654

655655
{{if interactive
656656

657657
```{test: no}
658-
class PMap {
658+
class PGroup {
659659
// Your code here
660660
}
661661
662-
let a = PMap.empty.set("a", 1);
663-
let ab = a.set("b", 2);
662+
let a = PGroup.empty.add("a");
663+
let ab = a.add("b");
664664
let b = ab.delete("a");
665665
666-
console.log(b.get("b"));
667-
// → 2
666+
console.log(b.has("b"));
667+
// → true
668668
console.log(a.has("b"));
669669
// → false
670670
console.log(b.has("a"));
@@ -675,23 +675,29 @@ if}}
675675

676676
{{hint
677677

678-
{{index "persistent map (exercise)", "Map class", array}}
678+
{{index "persistent map (exercise)", "Set class", array, "PGroup class"}}
679679

680-
Since this needs to work with keys that aren't strings, you can't use
681-
object properties to store the mapping. You could use a `Map` instance
682-
inside your `PMap` instance, or an array that stores a set of pairs.
680+
The most convenient way to represent the set of member values remains
681+
is still an array, since those are easy to copy.
683682

684-
I recommend using an array, since those are easy to copy. For each key
685-
added to the map, the array would hold a `{key, value}` object or a
686-
two-element array. When looking something up, you search the array for
687-
the pair that contains the proper key.
683+
{{index "concat method", "filter method"}}
688684

689-
The class' ((constructor)) would then take such an array as argument,
690-
and store it as the instance's (only) direct property. This array is
691-
never updated. The `set` and `delete` methods create copies of it,
692-
with the appropriate elements added and removed.
685+
When a value is added to the group, you can create a new group with a
686+
copy of the original array that has the value added (for example using
687+
`concat`). When a value is deleted, you filter it from the array.
693688

694-
When writing `set`, make sure that you also remove old versions of the
695-
key, in case a key that already exists is being set.
689+
The class' ((constructor)) would take such an array as argument, and
690+
store it as the instance's (only) property. This array is never
691+
updated.
692+
693+
{{index "static method"}}
694+
695+
To add a property to a constructor that is not a method, you have to
696+
add it to the constructor after the class definition, as a regular
697+
property.
698+
699+
You only need on `empty` instance because all empty groups are the
700+
same and instances of the class don't change. You can create many
701+
different groups from that single empty group without affecting it.
696702

697703
hint}}

11_async.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -997,16 +997,15 @@ the function returns (the one in the example never does), the iterator
997997
is done.
998998

999999
Writing iterators is often much easier when you use generator
1000-
functions. The iterator for a list object (from the exercise in
1001-
[Chapter ?](object#list_iterator)) can be written with this generator:
1000+
functions. The iterator for a group object (from the exercise in
1001+
[Chapter ?](object#group_iterator)) can be written with this generator:
10021002

1003-
{{index "List class"}}
1003+
{{index "Group class"}}
10041004

10051005
```
1006-
List.prototype[Symbol.iterator] = function*() {
1007-
for (let node = this; node != List.empty;
1008-
node = node.rest) {
1009-
yield node.value;
1006+
Group.prototype[Symbol.iterator] = function*() {
1007+
for (let i = 0; i < this.members.length; i++) {
1008+
yield this.members[i];
10101009
}
10111010
};
10121011
```

20_node.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ the `isDirectory` method tells us.
903903

904904
{{index "readdir function"}}
905905

906-
We use `readdir` to read the list of files in a ((directory)) and
906+
We use `readdir` to read the array of files in a ((directory)) and
907907
return it to the client. For normal files, we create a readable stream
908908
with `createReadStream` return that as the body, along with the
909909
content type that the `mime` package gives us based on the file's

21_skillsharing.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ and a _((client))_ part, written for the ((browser)). The server
4949
stores the system's data and provides it to the client. It also serves
5050
the files that implement the client-side system.
5151

52-
The server keeps a list of ((talk))s proposed for the next meeting,
52+
The server keeps the list of ((talk))s proposed for the next meeting,
5353
and the client shows this list. Each talk has a presenter name, a
54-
title, a summary, and a list of ((comment))s associated with it. The
54+
title, a summary, and an array of ((comment))s associated with it. The
5555
client allows users to propose new talks (adding them to the list),
5656
delete talks, and comment on existing talks. Whenever the user makes
5757
such a change, the client makes an ((HTTP)) ((request)) to tell the
@@ -548,18 +548,18 @@ be either a simple request for all talks or a long-polling request.
548548

549549
{{index "talkResponse method", "ETag header"}}
550550

551-
There will be multiple places in which we have to send a list of talks
552-
to the client, so we first define a helper method that builds up such
553-
a list and includes an `ETag` header in the response.
551+
There will be multiple places in which we have to send an array of
552+
talks to the client, so we first define a helper method that builds up
553+
such an array and includes an `ETag` header in the response.
554554

555555
```{includeCode: ">code/skillsharing/skillsharing_server.js"}
556556
SkillShareServer.prototype.talkResponse = function(talks) {
557-
let list = [];
557+
let talks = [];
558558
for (let title of Object.keys(this.talks)) {
559-
list.push(this.talks[title]);
559+
talks.push(this.talks[title]);
560560
}
561561
return {
562-
body: JSON.stringify(list),
562+
body: JSON.stringify(talks),
563563
headers: {"Content-Type": "application/json",
564564
"ETag": `"${this.version}"`}
565565
};

code/solutions/06_2_groups.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class Group {
2+
constructor() {
3+
this.members = [];
4+
}
5+
6+
add(value) {
7+
if (!this.members.includes(value)) {
8+
this.members.push(value);
9+
}
10+
}
11+
12+
delete(value) {
13+
this.members = this.members.filter(v => v !== value);
14+
}
15+
16+
has(value) {
17+
return this.members.includes(value);
18+
}
19+
20+
static from(collection) {
21+
let group = new Group;
22+
for (let value of collection) {
23+
group.add(value);
24+
}
25+
return group;
26+
}
27+
}
28+
29+
let group = Group.from([10, 20]);
30+
console.log(group.has(10));
31+
// → true
32+
console.log(group.has(30));
33+
// → false
34+
group.add(10);
35+
group.delete(10);
36+
console.log(group.has(10));

0 commit comments

Comments
 (0)