footer: © 2017 Reginald Braithwaite. Some rights reserved. slidenumbers: true autoscale: true
^ https://www.flickr.com/photos/fatedenied/7335413942
^ The Command Pattern
^ Note to the reader: This document is written in Markdown, a somewhat human-readable markup language. It is processed by a program called DeckSet, which turns it into a presentation. DeckSet does various things such as treating every paragraph that begins with a caret (^
) into a speaker's note that is not visible to the audience.
^ You may be looking at a partially-rendered version of this document, thanks to GitHub's ability to render basic MarkDown in the browser as formatted text. If so, you can look at the original document by clicking the Raw link above.
^ The fully rendered slides are on SpeakerDeck, and you can download a PDF there as well.
^ Some people have pointed out that DeckSet's MarkDown is not an excellent format for reading the material as compared to an essay. This is true. In its favour, you can't read a PowerPoint or KeyNote presentation's raw file at all, nor can you do interesting things like submit your own pull request on GitHub, so MarkDown does have some interesting affordances.
^ Thanks for reading this, and feel free to post an Issue.
^ https://www.flickr.com/photos/fatedenied/7335413942
why do we care about commands?
^ https://www.flickr.com/photos/fatedenied/7335413942
^ Let's get started
class Buffer {
constructor (text = '') { this.text = text; }
replaceWith (replacement, from = 0, to = this.text.length) {
this.text = this.text.slice(0, from) +
replacement +
this.text.slice(to);
return this;
}
toString () { return this.text; }
}
let buffer = new Buffer();
buffer.replaceWith(
"The quick brown fox jumped over the lazy dog"
);
buffer.replaceWith("fast", 4, 9);
buffer.replaceWith("canine", 40, 43);
//=> The fast brown fox jumped over the lazy canine
^ https://www.flickr.com/photos/mwichary/2406482529
^ https://www.flickr.com/photos/tompagenet/8580371564
^ e.g. decorators
^ https://www.flickr.com/photos/ooocha/2869485136
^ Talk about invocations being ephemeral and tied up with mutable state, some visible, some hidden.
^ https://www.flickr.com/photos/oskay/2550938136
class Edit {
constructor (buffer, {replacement, from, to}) {
this.buffer = buffer;
Object.assign(this, {replacement, from, to});
}
doIt () {
this.buffer.text =
this.buffer.text.slice(0, this.from) +
this.replacement +
this.buffer.text.slice(this.to);
return this.buffer;
}
}
class Buffer {
constructor (text = '') { this.text = text; }
replaceWith (replacement, from = 0, to = this.text.length) {
return new Edit(this, {replacement, from, to});
}
toString () { return this.text; }
}
let buffer = new Buffer(), jobQueue = [];
jobQueue.push(
buffer.replaceWith(
"The quick brown fox jumped over the lazy dog"
)
);
jobQueue.push( buffer.replaceWith("fast", 4, 9) );
jobQueue.push( buffer.replaceWith("canine", 40, 43) );
while (jobQueue.length > 0) {
jobQueue.shift().doIt();
}
//=> The fast brown fox jumped over the lazy canine
^ Presto, a job queue!
^ https://www.flickr.com/photos/eschipul/1219204898) ^ Deferred execution ^ Asynchronous execution
^ chaining and handling serialization ^ cancel ^ ember concurrency: task instances are like commands, tasks are like job queues
^ https://www.flickr.com/photos/baccharus/4474584940
class Edit {
netChange () {
return this.from - this.to + this.replacement.length;
}
}
let buffer = new Buffer();
buffer.replaceWith(
"The quick brown fox jumped over the lazy dog"
).netChange();
//=> 44
buffer.replaceWith("fast", 4, 9).netChange();
//=> -1
^ status ^ queue control versus debouncing functions
^ logs as first-class entities ^ tracking behaviour
^ traditional logs are terrible and opaque ^ why litter our UX code with tracking invocations?
^ https://www.flickr.com/photos/micurs/4906349993
^ combinators, combinators, combinators
class Edit {
reversed () {
let replacement = this.buffer.text.slice(this.from, this.to),
from = this.from,
to = from + this.replacement.length;
return new Edit(buffer, {replacement, from, to});
}
}
let buffer = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
let doer = buffer.replaceWith("fast", 4, 9),
undoer = doer.reversed();
doer.doIt();
//=> The fast brown fox jumped over the lazy dog
undoer.doIt();
//=> The quick brown fox jumped over the lazy dog
^ https://www.flickr.com/photos/purdman1/2875431305
class Buffer {
constructor (text = '') {
this.text = text;
this.history = [];
this.future = [];
}
}
class Buffer {
replaceWith (replacement, from = 0, to = this.length()) {
let doer = new Edit(this, {replacement, from, to}),
undoer = doer.reversed();
this.history.push(undoer);
this.future = [];
return doer.doIt();
}
}
^ https://www.flickr.com/photos/daryl_mitchell/15427050433
class Buffer {
undo () {
let undoer = this.history.pop(),
redoer = undoer.reversed();
this.future.unshift(redoer);
return undoer.doIt();
}
}
let buffer = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
buffer.replaceWith("fast", 4, 9)
//=> The fast brown fox jumped over the lazy dog
buffer.replaceWith("canine", 40, 43)
//=> The fast brown fox jumped over the lazy canine
buffer.undo()
//=> The fast brown fox jumped over the lazy dog
buffer.undo()
//=> The quick brown fox jumped over the lazy dog
^ https://www.flickr.com/photos/the00rig/3753005997
class Buffer {
redo () {
let redoer = this.future.shift(),
undoer = redoer.reversed();
this.history.push(undoer);
return redoer.doIt();
}
}
buffer.redo()
//=> The fast brown fox jumped over the lazy dog
buffer.redo()
//=> The fast brown fox jumped over the lazy canine
^ https://www.flickr.com/photos/robbie1/8656027235
^ https://www.flickr.com/photos/mwichary/2406489333
^ https://www.flickr.com/photos/pedrosimoes7/17386505158
class Buffer {
replaceWith (replacement, from = 0, to = this.length()) {
let doer = new Edit(this, {replacement, from, to}),
undoer = doer.reversed();
this.history.push(undoer);
this.future = [];
return doer.doIt();
}
}
^ https://www.flickr.com/photos/a-barth/2846621384
class Buffer {
replaceWith (replacement, from = 0, to = this.length()) {
let doer = new Edit(this, {replacement, from, to}),
undoer = doer.reversed();
this.history.push(undoer);
// this.future = [];
return doer.doIt();
}
}
let buffer = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
buffer.replaceWith("fast", 4, 9);
//=> The fast brown fox jumped over the lazy dog
buffer.undo();
//=> The quick brown fox jumped over the lazy dog
buffer.replaceWith("My", 0, 3);
//=> My quick brown fox jumped over the lazy dog
^ https://www.flickr.com/photos/mleung311/9468927282
^ https://www.flickr.com/photos/bludgeoner86/5590795033
^ because our commands are coupled to ephemeral state, changing the state breaks the command
^ https://www.flickr.com/photos/49024304@N00/
"The quick brown fox jumped over the lazy dog"
// PAST
// FUTURE
"The fast brown fox jumped over the lazy dog"
// PAST
replaceWith("fast", 4, 9)
// FUTURE
"The quick brown fox jumped over the lazy dog"
// PAST
// FUTURE
replaceWith("fast", 4, 9)
"My quick brown fox jumped over the lazy dog"
// PAST
replaceWith("My", 0, 3)
// FUTURE
replaceWith("fast", 4, 9)
"My qfastbrown fox jumped over the lazy dog"
// PAST
replaceWith("My", 0, 3)
replaceWith("fast", 4, 9)
// FUTURE
^ https://www.flickr.com/photos/29143375@N05/4575806708
^ https://www.flickr.com/photos/30239838@N04/4268147953
let buffer = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
let fast = new Edit(
buffer,
{ replacement: "fast", from: 4, to: 9 }
);
let my = new Edit(
buffer,
{ replacement: "My", from: 0, to: 3 }
);
let buffer = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
let fast = new Edit(
buffer,
{ replacement: "fast", from: 4, to: 9 }
);
let my = new Edit(
buffer,
{ replacement: "My", from: 0, to: 3 }
);
class Edit {
isBefore (other) {
return other.from >= this.to;
}
}
fast.isBefore(my);
//=> false
my.isBefore(fast);
//=> true
class Edit {
prependedWith (other) {
if (this.isBefore(other)) {
return this;
}
else if (other.isBefore(this)) {
let change = other.netChange(),
{replacement, from, to} = this;
from = from + change;
to = to + change;
return new Edit(this.buffer, {replacement, from, to})
}
}
}
my.prependedWith(fast)
//=> buffer.replaceWith("My", 0, 3)
fast.prependedWith(my)
//=> buffer.replaceWith("fast", 3, 8)
my.prependedWith(fast)
//=> buffer.replaceWith("My", 0, 3)
fast.prependedWith(my)
//=> buffer.replaceWith("fast", 3, 8)
class Buffer {
replaceWith (replacement, from = 0, to = this.length()) {
let doer = new Edit(this, {replacement, from, to}),
undoer = doer.reversed();
this.history.push(undoer);
this.future = this.future.map(
(edit) => edit.prependedWith(doer)
);
return doer.doIt();
}
}
^ https://www.flickr.com/photos/benetd/4429314827
let buffer = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
buffer.replaceWith("fast", 4, 9);
//=> The fast brown fox jumped over the lazy dog
buffer.undo();
//=> The quick brown fox jumped over the lazy dog
buffer.replaceWith("My", 0, 3);
//=> My quick brown fox jumped over the lazy dog
buffer.redo();
^ https://www.flickr.com/photos/katiethebeau/16670836007
"People assume that time is a strict progression of cause to effect, but actually from a non-linear, non-subjective viewpoint—it's more like a big ball of wibbly wobbly… time-y wimey… stuff."
^ https://www.flickr.com/photos/shimgray/2811100997
^ https://www.youtube.com/watch?v=mDsN5lWLKU0
^ "Alice B. Toklas and Bob Fosse are editing a script"
let alice = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
let bob = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
class Buffer {
constructor (text = '') {
this.text = text;
this.history = [];
}
}
class Buffer {
replaceWith (replacement, from = 0, to = this.length()) {
let edit = new Edit(this,
{replacement, from, to}
);
this.history.push(edit);
return edit.doIt();
}
}
alice.replaceWith("My", 0, 3);
//=> My quick brown fox jumped over the lazy dog
bob.replaceWith("fast", 4, 9);
//=> The fast brown fox jumped over the lazy dog
class Buffer {
append (theirEdit) {
this.history.forEach( (myEdit) => {
theirEdit = theirEdit.prependedWith(myEdit);
});
return new Edit(this, theirEdit).doIt();
}
}
class Buffer {
appendAll(otherBuffer) {
otherBuffer.history.forEach(
(theirEdit) => this.append(theirEdit)
);
return this;
}
}
alice.appendAll(bob);
//=> My fast brown fox jumped over the lazy dog
bob.appendAll(alice);
//=> My fast brown fox jumped over the lazy dog
^ "We have a little bug."
^ "Okay, a big bug! We can't appendAll more than once. Shared mutable data everywhere."
let GUID = () => ???
class Buffer {
constructor (text = '', history = []) {
let befores = new Set(history.map(e => e.guid));
history = history.slice(0);
Object.assign(this, {text, history, befores});
}
share () {
return new Buffer(this.text, this.history);
}
}
^ We're going to use guids and a set of before guids in the buffer and the edits
^ let GUID = () => { ^ let _p8 = (s) => { ^ let p = (Math.random().toString(16)+"000000000").substr(2,8); ^ ^ return s ? "-" + p.substr(0,4) + "-" + p.substr(4,4) : p ; ^ } ^ return _p8() + _p8(true) + _p8(true) + _p8(); ^ }
class Edit {
constructor (buffer,
{ guid = GUID(), befores = new Set(),
replacement, from, to }) {
this.buffer = buffer;
befores = new Set(befores);
Object.assign(this,
{guid, replacement, from, to, befores});
}
}
class Buffer {
has (edit) { return this.befores.has(edit.guid); }
perform (edit) {
if (!this.has(edit)) {
this.history.push(edit);
this.befores.add(edit.guid);
return edit.doIt();
}
}
}
^ now we check edits before we perform them
class Buffer {
replaceWith (replacement,
from = 0, to = this.length()) {
let befores = this.befores,
let edit = new Edit(this,
{replacement, from, to, befores}
);
return this.perform(edit);
}
}
^ simplifies replaceWith
, append
, and appendAll
class Buffer {
append (theirEdit) {
this.history.forEach( (myEdit) => {
theirEdit = theirEdit.prependedWith(myEdit);
});
return this.perform(new Edit(this, theirEdit));
}
}
class Buffer {
appendAll(otherBuffer) {
otherBuffer.history.forEach(
(theirEdit) =>
this.has(theirEdit) || this.append(theirEdit)
);
return this;
}
}
class Edit {
prependedWith (other) {
if (this.isBefore(other) ||
this.befores.has(other.guid) ||
this.guid === other.guid) return this;
let change = other.netChange(),
{guid, replacement, from, to, befores} = this;
from = from + change;
to = to + change;
befores = new Set(befores);
befores.add(other.guid);
return new Edit(this.buffer, {guid, replacement, from, to, befores});
}
}
^ "Alice, Bob, and Carol are editing a script"
let alice = new Buffer(
"The quick brown fox jumped over the lazy dog"
);
let bob = alice.share();
//=> The quick brown fox jumped over the lazy dog
alice.replaceWith("My", 0, 3);
//=> My quick brown fox jumped over the lazy dog
let carol = alice.share();
//=> My quick brown fox jumped over the lazy dog
bob.replaceWith("fast", 4, 9);
//=> The fast brown fox jumped over the lazy dog
alice.appendAll(bob);
//=> My fast brown fox jumped over the lazy dog
bob.appendAll(alice);
//=> My fast brown fox jumped over the lazy dog
alice.replaceWith("spotted", 8, 13);
//=> My fast spotted fox jumped over the lazy dog
bob.appendAll(alice);
//=> My fast spotted fox jumped over the lazy dog
carol.appendAll(bob);
//=> My fast spotted fox jumped over the lazy dog
"Unfortunately, implementing OT sucks. There's a million algorithms with different tradeoffs, mostly trapped in academic papers. The algorithms are really hard and time consuming to implement correctly."
^ https://www.flickr.com/photos/wordridden/4308645407
^ Joseph Gentle, from https://en.wikipedia.org/wiki/Operational_transformation
^ too much responsibility in edits, and we have an XY problem
perhaps we should borrow a trick from react, and periodically scan a "shadow buffer" for diffs that we exchange with collaborators…
^ congratulations, now we're talking differential synchronization. See Electron, Google Docs
^ See also: https://neil.fraser.name/writing/sync/
^ lose the semantics
^ but more flexible!
^ https://www.flickr.com/photos/sidelong/18620995913
^ There are only two hard problems in computer science: Cache invalidation, and naming things--Phil Karleton
^ leads into discussing what we call merging and version control
class Buffer {
replaceWith () { ... }
share () { ... }
append () { ... }
appendAll () { ... }
}
class Branch {
commit () { ... }
fork () { ... }
cherryPick () { ... }
merge () { ... }
}
^ git is differential
^ what we lose is semantics like "rename this variable"
^ what we win is flexibility: any tool can perform edits
with invocations as first-class entities, we can build distributed algorithms and protocols; we can master time and change
^ https://www.flickr.com/photos/stawarz/3848824508
^ today's users expect real-time and collaborative applications
^ real-time and interactive is the default. we really need to take another look at EVERY single-user batch-and-submit UX interaction and rethink it.
^ Why can't support help me fill out a form?
^ Why can't multiple people collaboratively edit a form?
^ Why don't things we save have version control?
^ Why can't we merge changes from multiple forks of a form?
^ https://www.flickr.com/photos/mwichary/3338901313
^ These are excellent ideas, but somewhat "boil the ocean."
^ What can we do modestly? How do we get started?
^ https://www.flickr.com/photos/churchhatestucker/2472583642
^ this is the big wrap-up: get a lot of code out of the UX and out of the persistence mechanism