First-Class Commands, the 2017 Edition

First-Class Commands, the 2017 Edition

NDC Oslo June 15, 2017

F8f7496052d3bf856e944aec64cfbb99?s=128

Reg Braithwaite

June 15, 2017
Tweet

Transcript

  1. © 2017 Reginald Braithwaite. Some rights reserved. 1

  2. First-Class Commands an unexpectedly fertile design pa!ern © 2017 Reginald

    Braithwaite. Some rights reserved. 2
  3. why do we care about commands? © 2017 Reginald Braithwaite.

    Some rights reserved. 3
  4. © 2017 Reginald Braithwaite. Some rights reserved. 4

  5. the canonical example: mutable data © 2017 Reginald Braithwaite. Some

    rights reserved. 5
  6. 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; } } © 2017 Reginald Braithwaite. Some rights reserved. 6
  7. 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 © 2017 Reginald Braithwaite. Some rights reserved. 7
  8. buffer is an object © 2017 Reginald Braithwaite. Some rights

    reserved. 8
  9. we treat objects as first-class entities © 2017 Reginald Braithwaite.

    Some rights reserved. 9
  10. replaceWith is a method © 2017 Reginald Braithwaite. Some rights

    reserved. 10
  11. we can treat methods as first- class entities © 2017

    Reginald Braithwaite. Some rights reserved. 11
  12. buffer.replaceWith("fast", 4, 9) is an invocation © 2017 Reginald Braithwaite.

    Some rights reserved. 12
  13. what does it mean to treat an invocation as a

    first-class entity? © 2017 Reginald Braithwaite. Some rights reserved. 13
  14. storing invocations © 2017 Reginald Braithwaite. Some rights reserved. 14

  15. 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; } } © 2017 Reginald Braithwaite. Some rights reserved. 15
  16. 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; } } © 2017 Reginald Braithwaite. Some rights reserved. 16
  17. 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 © 2017 Reginald Braithwaite. Some rights reserved. 17
  18. job queues © 2017 Reginald Braithwaite. Some rights reserved. 18

  19. © 2017 Reginald Braithwaite. Some rights reserved. 19

  20. querying invocations © 2017 Reginald Braithwaite. Some rights reserved. 20

  21. class Edit { netChange () { return this.from - this.to

    + this.replacement.length; } } © 2017 Reginald Braithwaite. Some rights reserved. 21
  22. let buffer = new Buffer(); buffer.replaceWith( "The quick brown fox

    jumped over the lazy dog" ).netChange(); //=> 44 buffer.replaceWith("fast", 4, 9).netChange(); //=> -1 © 2017 Reginald Braithwaite. Some rights reserved. 22
  23. command state © 2017 Reginald Braithwaite. Some rights reserved. 23

  24. © 2017 Reginald Braithwaite. Some rights reserved. 24

  25. command history © 2017 Reginald Braithwaite. Some rights reserved. 25

  26. © 2017 Reginald Braithwaite. Some rights reserved. 26

  27. transforming invocations © 2017 Reginald Braithwaite. Some rights reserved. 27

  28. 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}); } } © 2017 Reginald Braithwaite. Some rights reserved. 28
  29. 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 © 2017 Reginald Braithwaite. Some rights reserved. 29
  30. all together now © 2017 Reginald Braithwaite. Some rights reserved.

    30
  31. class Buffer { constructor (text = '') { this.text =

    text; this.history = []; this.future = []; } } © 2017 Reginald Braithwaite. Some rights reserved. 31
  32. 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(); } } © 2017 Reginald Braithwaite. Some rights reserved. 32
  33. undo © 2017 Reginald Braithwaite. Some rights reserved. 33

  34. class Buffer { undo () { let undoer = this.history.pop(),

    redoer = undoer.reversed(); this.future.unshift(redoer); return undoer.doIt(); } } © 2017 Reginald Braithwaite. Some rights reserved. 34
  35. 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 © 2017 Reginald Braithwaite. Some rights reserved. 35
  36. buffer.undo() //=> The fast brown fox jumped over the lazy

    dog buffer.undo() //=> The quick brown fox jumped over the lazy dog © 2017 Reginald Braithwaite. Some rights reserved. 36
  37. redo © 2017 Reginald Braithwaite. Some rights reserved. 37

  38. class Buffer { redo () { let redoer = this.future.shift(),

    undoer = redoer.reversed(); this.history.push(undoer); return redoer.doIt(); } } © 2017 Reginald Braithwaite. Some rights reserved. 38
  39. buffer.redo() //=> The fast brown fox jumped over the lazy

    dog buffer.redo() //=> The fast brown fox jumped over the lazy canine © 2017 Reginald Braithwaite. Some rights reserved. 39
  40. that's the basic command pa!ern © 2017 Reginald Braithwaite. Some

    rights reserved. 40
  41. invocations as first-class entities: we stored them; we queried them;

    we transformed them. © 2017 Reginald Braithwaite. Some rights reserved. 41
  42. question! © 2017 Reginald Braithwaite. Some rights reserved. 42

  43. 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(); } } © 2017 Reginald Braithwaite. Some rights reserved. 43
  44. why do we have to throw the future away? ©

    2017 Reginald Braithwaite. Some rights reserved. 44
  45. 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(); } } © 2017 Reginald Braithwaite. Some rights reserved. 45
  46. 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 © 2017 Reginald Braithwaite. Some rights reserved. 46
  47. what happens when we evaluate buffer.redo()? © 2017 Reginald Braithwaite.

    Some rights reserved. 47
  48. "My qfastbrown fox jumped over the lazy dog" © 2017

    Reginald Braithwaite. Some rights reserved. 48
  49. © 2017 Reginald Braithwaite. Some rights reserved. 49

  50. let's consider commands as a history © 2017 Reginald Braithwaite.

    Some rights reserved. 50
  51. let buffer = new Buffer("The quick brown fox jumped over

    the lazy dog"); "The quick brown fox jumped over the lazy dog" // PAST // FUTURE © 2017 Reginald Braithwaite. Some rights reserved. 51
  52. buffer.replaceWith("fast", 4, 9) "The fast brown fox jumped over the

    lazy dog" // PAST replaceWith("fast", 4, 9) // FUTURE © 2017 Reginald Braithwaite. Some rights reserved. 52
  53. buffer.undo() "The quick brown fox jumped over the lazy dog"

    // PAST // FUTURE replaceWith("fast", 4, 9) © 2017 Reginald Braithwaite. Some rights reserved. 53
  54. buffer.replaceWith("My", 0, 3) "My quick brown fox jumped over the

    lazy dog" // PAST replaceWith("My", 0, 3) // FUTURE replaceWith("fast", 4, 9) © 2017 Reginald Braithwaite. Some rights reserved. 54
  55. buffer.redo() "My qfastbrown fox jumped over the lazy dog" //

    PAST replaceWith("My", 0, 3) replaceWith("fast", 4, 9) // FUTURE © 2017 Reginald Braithwaite. Some rights reserved. 55
  56. every command depends on the history of commands preceding it

    © 2017 Reginald Braithwaite. Some rights reserved. 56
  57. prepending a command into its history alters the command ©

    2017 Reginald Braithwaite. Some rights reserved. 57
  58. 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 } ); © 2017 Reginald Braithwaite. Some rights reserved. 58
  59. 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 } ); © 2017 Reginald Braithwaite. Some rights reserved. 59
  60. class Edit { isBefore (other) { return other.from >= this.to;

    } } fast.isBefore(my); //=> false my.isBefore(fast); //=> true © 2017 Reginald Braithwaite. Some rights reserved. 60
  61. 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}) } } } © 2017 Reginald Braithwaite. Some rights reserved. 61
  62. my.prependedWith(fast) //=> buffer.replaceWith("My", 0, 3) fast.prependedWith(my) //=> buffer.replaceWith("fast", 3, 8)

    © 2017 Reginald Braithwaite. Some rights reserved. 62
  63. my.prependedWith(fast) //=> buffer.replaceWith("My", 0, 3) fast.prependedWith(my) //=> buffer.replaceWith("fast", 3, 8)

    © 2017 Reginald Braithwaite. Some rights reserved. 63
  64. 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(); } } © 2017 Reginald Braithwaite. Some rights reserved. 64
  65. let's start over © 2017 Reginald Braithwaite. Some rights reserved.

    65
  66. 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(); © 2017 Reginald Braithwaite. Some rights reserved. 66
  67. "My fast brown fox jumped over the lazy dog" ©

    2017 Reginald Braithwaite. Some rights reserved. 67
  68. what did fixing redo teach us about invocations as first-

    class entities? © 2017 Reginald Braithwaite. Some rights reserved. 68
  69. "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." © 2017 Reginald Braithwaite. Some rights reserved. 69
  70. reversed() and prependedWith() show us we can change both the

    direction and order of time. © 2017 Reginald Braithwaite. Some rights reserved. 70
  71. © 2017 Reginald Braithwaite. Some rights reserved. 71

  72. 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" ); © 2017 Reginald Braithwaite. Some rights reserved. 72
  73. for simplicity, we'll omit undo , redo and reversed class

    Buffer { constructor (text = '') { this.text = text; this.history = []; } } © 2017 Reginald Braithwaite. Some rights reserved. 73
  74. class Buffer { replaceWith (replacement, from = 0, to =

    this.length()) { let edit = new Edit(this, {replacement, from, to} ); this.history.push(edit); return edit.doIt(); } } © 2017 Reginald Braithwaite. Some rights reserved. 74
  75. 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 © 2017 Reginald Braithwaite. Some rights reserved. 75
  76. © 2017 Reginald Braithwaite. Some rights reserved. 76

  77. class Buffer { append (theirEdit) { this.history.forEach( (myEdit) => {

    theirEdit = theirEdit.prependedWith(myEdit); }); return new Edit(this, theirEdit).doIt(); } } © 2017 Reginald Braithwaite. Some rights reserved. 77
  78. class Buffer { appendAll(otherBuffer) { otherBuffer.history.forEach( (theirEdit) => this.append(theirEdit) );

    return this; } } © 2017 Reginald Braithwaite. Some rights reserved. 78
  79. alice.appendAll(bob); //=> My fast brown fox jumped over the lazy

    dog bob.appendAll(alice); //=> My fast brown fox jumped over the lazy dog © 2017 Reginald Braithwaite. Some rights reserved. 79
  80. ! © 2017 Reginald Braithwaite. Some rights reserved. 80

  81. © 2017 Reginald Braithwaite. Some rights reserved. 81

  82. 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); } } © 2017 Reginald Braithwaite. Some rights reserved. 82
  83. 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}); } } © 2017 Reginald Braithwaite. Some rights reserved. 83
  84. 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(); } } } © 2017 Reginald Braithwaite. Some rights reserved. 84
  85. 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); } } © 2017 Reginald Braithwaite. Some rights reserved. 85
  86. class Buffer { append (theirEdit) { this.history.forEach( (myEdit) => {

    theirEdit = theirEdit.prependedWith(myEdit); }); return this.perform(new Edit(this, theirEdit)); } } © 2017 Reginald Braithwaite. Some rights reserved. 86
  87. class Buffer { appendAll(otherBuffer) { otherBuffer.history.forEach( (theirEdit) => this.has(theirEdit) ||

    this.append(theirEdit) ); return this; } } © 2017 Reginald Braithwaite. Some rights reserved. 87
  88. 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}); } } © 2017 Reginald Braithwaite. Some rights reserved. 88
  89. © 2017 Reginald Braithwaite. Some rights reserved. 89

  90. 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 © 2017 Reginald Braithwaite. Some rights reserved. 90
  91. 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 © 2017 Reginald Braithwaite. Some rights reserved. 91
  92. 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 © 2017 Reginald Braithwaite. Some rights reserved. 92
  93. "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." © 2017 Reginald Braithwaite. Some rights reserved. 93
  94. perhaps we should borrow a trick from react, and periodically

    scan a "shadow buffer" for diffs that we exchange with collaborators… © 2017 Reginald Braithwaite. Some rights reserved. 94
  95. differential synchronization © 2017 Reginald Braithwaite. Some rights reserved. 95

  96. how does differential synchronization differ from the command pa!ern? ©

    2017 Reginald Braithwaite. Some rights reserved. 96
  97. this is a very big problem space © 2017 Reginald

    Braithwaite. Some rights reserved. 97
  98. © 2017 Reginald Braithwaite. Some rights reserved. 98

  99. class Buffer { replaceWith () { ... } share ()

    { ... } append () { ... } appendAll () { ... } } © 2017 Reginald Braithwaite. Some rights reserved. 99
  100. class Branch { commit () { ... } fork ()

    { ... } cherryPick () { ... } merge () { ... } } © 2017 Reginald Braithwaite. Some rights reserved. 100
  101. distributed version control © 2017 Reginald Braithwaite. Some rights reserved.

    101
  102. with invocations as first-class entities, we can build distributed algorithms

    and protocols; we can master time and change © 2017 Reginald Braithwaite. Some rights reserved. 102
  103. © 2017 Reginald Braithwaite. Some rights reserved. 103

  104. © 2017 Reginald Braithwaite. Some rights reserved. 104

  105. down, tiger! © 2017 Reginald Braithwaite. Some rights reserved. 105

  106. separation of concerns © 2017 Reginald Braithwaite. Some rights reserved.

    106
  107. Reg Braithwaite PagerDuty, Inc. h!ps://speakerdeck.com/raganwald/ first-class-commands-an- unexpectedly-fertile-design-pa!ern raganwald.com @raganwald ©

    2017 Reginald Braithwaite. Some rights reserved. 107