From 0b920df2721acd95a663c4682caf3c5d860cf91c Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 08:47:37 -0400 Subject: [PATCH 01/14] Diffing by key handles equal terms correctly. --- prototype/Doubt/Algorithm.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/prototype/Doubt/Algorithm.swift b/prototype/Doubt/Algorithm.swift index cf522c9cd..a5ed51bf8 100644 --- a/prototype/Doubt/Algorithm.swift +++ b/prototype/Doubt/Algorithm.swift @@ -90,6 +90,11 @@ public enum FreeAlgorithm { : Diff.Pure(.Replace(a, b))).evaluate(equals, recur: recur) case let .Roll(.ByKey(a, b, f)): + let recur = { + Term.equals(equals)($0, $1) + ? Diff($1) + : recur($0, $1) + } let deleted = Set(a.keys).subtract(b.keys).map { ($0, Diff.Pure(Patch.Delete(a[$0]!))) } let inserted = Set(b.keys).subtract(a.keys).map { ($0, Diff.Pure(Patch.Insert(b[$0]!))) } let patched = Set(a.keys).intersect(b.keys).map { ($0, recur(a[$0]!, b[$0]!)) } From fa7a89c247086e51168e9f0dfff0aebc2b932827 Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 12:59:22 -0400 Subject: [PATCH 02/14] Rename Algorithm to Operation. --- prototype/Doubt/Algorithm.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/prototype/Doubt/Algorithm.swift b/prototype/Doubt/Algorithm.swift index a5ed51bf8..718a1f2d9 100644 --- a/prototype/Doubt/Algorithm.swift +++ b/prototype/Doubt/Algorithm.swift @@ -1,5 +1,5 @@ /// An operation of diffing over terms or collections of terms. -public enum Algorithm { +public enum Operation { /// The type of `Term`s over which `Algorithm`s operate. public typealias Term = Fix @@ -20,7 +20,7 @@ public enum Algorithm { // MARK: Functor - public func map(transform: Recur -> Other) -> Algorithm { + public func map(transform: Recur -> Other) -> Operation { switch self { case let .Recursive(a, b, f): return .Recursive(a, b, f >>> transform) @@ -40,10 +40,10 @@ public enum Algorithm { /// Where `Algorithm` models a single diffing strategy, `FreeAlgorithm` models the recursive selection of diffing strategies at each node. Thus, a value in `FreeAlgorithm` models an algorithm for constructing a value in the type `B` from the resulting diffs. By this means, diffing can be adapted not just to the specific grammar, but to specific trees produced by that grammar, and even the values of type `A` encapsulated at each node. public enum FreeAlgorithm { /// The type of `Term`s over which `FreeAlgorithm`s operate. - public typealias Term = Algorithm.Term + public typealias Term = Operation.Term /// The type of `Diff`s which `FreeAlgorithm`s produce. - public typealias Diff = Algorithm.Diff + public typealias Diff = Operation.Diff /// The injection of a value of type `B` into an `Algorithm`. /// @@ -51,9 +51,9 @@ public enum FreeAlgorithm { case Pure(B) /// A recursive instantiation of `Algorithm`, unrolling another iteration of the recursive type. - case Roll(Algorithm) + case Roll(Operation) - public func analysis(@noescape ifPure ifPure: B -> C, @noescape ifRoll: Algorithm -> C) -> C { + public func analysis(@noescape ifPure ifPure: B -> C, @noescape ifRoll: Operation -> C) -> C { switch self { case let .Pure(b): return ifPure(b) From b8b5a26b46aa8821cac599adccb96d36390f3732 Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:00:17 -0400 Subject: [PATCH 03/14] Update doc comments. --- prototype/Doubt/Algorithm.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/prototype/Doubt/Algorithm.swift b/prototype/Doubt/Algorithm.swift index 718a1f2d9..43f6f5329 100644 --- a/prototype/Doubt/Algorithm.swift +++ b/prototype/Doubt/Algorithm.swift @@ -1,9 +1,9 @@ /// An operation of diffing over terms or collections of terms. public enum Operation { - /// The type of `Term`s over which `Algorithm`s operate. + /// The type of `Term`s over which `Operation`s operate. public typealias Term = Fix - /// The type of `Diff`s which `Algorithm`s produce. + /// The type of `Diff`s which `Operation`s produce. public typealias Diff = Free> /// Indicates that diffing should compare the enclosed `Term`s. @@ -33,11 +33,11 @@ public enum Operation { } -/// The free monad over `Algorithm`, implementing the language of diffing. +/// The free monad over `Operation`, implementing the language of diffing. /// -/// As with `Free`, this is “free” in the sense of “unconstrained,” i.e. “the monad induced by `Algorithm` without extra assumptions.” +/// As with `Free`, this is “free” in the sense of “unconstrained,” i.e. “the monad induced by `Operation` without extra assumptions.” /// -/// Where `Algorithm` models a single diffing strategy, `FreeAlgorithm` models the recursive selection of diffing strategies at each node. Thus, a value in `FreeAlgorithm` models an algorithm for constructing a value in the type `B` from the resulting diffs. By this means, diffing can be adapted not just to the specific grammar, but to specific trees produced by that grammar, and even the values of type `A` encapsulated at each node. +/// Where `Operation` models a single diffing strategy, `FreeAlgorithm` models the recursive selection of diffing strategies at each node. Thus, a value in `FreeAlgorithm` models an algorithm for constructing a value in the type `B` from the resulting diffs. By this means, diffing can be adapted not just to the specific grammar, but to specific trees produced by that grammar, and even the values of type `A` encapsulated at each node. public enum FreeAlgorithm { /// The type of `Term`s over which `FreeAlgorithm`s operate. public typealias Term = Operation.Term @@ -45,12 +45,12 @@ public enum FreeAlgorithm { /// The type of `Diff`s which `FreeAlgorithm`s produce. public typealias Diff = Operation.Diff - /// The injection of a value of type `B` into an `Algorithm`. + /// The injection of a value of type `B` into an `Operation`. /// /// Equally, a way to return a result or throw an error during computation, as determined by the type which `B` is instantiated to, and the specific context in which it is being evaluated. case Pure(B) - /// A recursive instantiation of `Algorithm`, unrolling another iteration of the recursive type. + /// A recursive instantiation of `Operation`, unrolling another iteration of the recursive type. case Roll(Operation) public func analysis(@noescape ifPure ifPure: B -> C, @noescape ifRoll: Operation -> C) -> C { From ecef6f837037b74aa772e285da890a77b4f3ceec Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:00:54 -0400 Subject: [PATCH 04/14] Rename `FreeAlgorithm` to `Algorithm`. --- prototype/Doubt/Algorithm.swift | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/prototype/Doubt/Algorithm.swift b/prototype/Doubt/Algorithm.swift index 43f6f5329..3a28e1d8c 100644 --- a/prototype/Doubt/Algorithm.swift +++ b/prototype/Doubt/Algorithm.swift @@ -37,13 +37,13 @@ public enum Operation { /// /// As with `Free`, this is “free” in the sense of “unconstrained,” i.e. “the monad induced by `Operation` without extra assumptions.” /// -/// Where `Operation` models a single diffing strategy, `FreeAlgorithm` models the recursive selection of diffing strategies at each node. Thus, a value in `FreeAlgorithm` models an algorithm for constructing a value in the type `B` from the resulting diffs. By this means, diffing can be adapted not just to the specific grammar, but to specific trees produced by that grammar, and even the values of type `A` encapsulated at each node. -public enum FreeAlgorithm { - /// The type of `Term`s over which `FreeAlgorithm`s operate. - public typealias Term = Operation.Term +/// Where `Operation` models a single diffing strategy, `Algorithm` models the recursive selection of diffing strategies at each node. Thus, a value in `Algorithm` models an algorithm for constructing a value in the type `B` from the resulting diffs. By this means, diffing can be adapted not just to the specific grammar, but to specific trees produced by that grammar, and even the values of type `A` encapsulated at each node. +public enum Algorithm { + /// The type of `Term`s over which `Algorithm`s operate. + public typealias Term = Operation.Term - /// The type of `Diff`s which `FreeAlgorithm`s produce. - public typealias Diff = Operation.Diff + /// The type of `Diff`s which `Algorithm`s produce. + public typealias Diff = Operation.Diff /// The injection of a value of type `B` into an `Operation`. /// @@ -51,9 +51,9 @@ public enum FreeAlgorithm { case Pure(B) /// A recursive instantiation of `Operation`, unrolling another iteration of the recursive type. - case Roll(Operation) + case Roll(Operation) - public func analysis(@noescape ifPure ifPure: B -> C, @noescape ifRoll: Operation -> C) -> C { + public func analysis(@noescape ifPure ifPure: B -> C, @noescape ifRoll: Operation -> C) -> C { switch self { case let .Pure(b): return ifPure(b) @@ -65,14 +65,14 @@ public enum FreeAlgorithm { // MARK: Functor - public func map(transform: B -> Other) -> FreeAlgorithm { - return analysis(ifPure: transform >>> FreeAlgorithm.Pure, ifRoll: { .Roll($0.map { $0.map(transform) }) }) + public func map(transform: B -> Other) -> Algorithm { + return analysis(ifPure: transform >>> Algorithm.Pure, ifRoll: { .Roll($0.map { $0.map(transform) }) }) } // MARK: Monad - public func flatMap(transform: B -> FreeAlgorithm) -> FreeAlgorithm { + public func flatMap(transform: B -> Algorithm) -> Algorithm { return analysis(ifPure: transform, ifRoll: { .Roll($0.map { $0.flatMap(transform) }) }) } @@ -106,7 +106,7 @@ public enum FreeAlgorithm { } } -extension FreeAlgorithm where A: Equatable { +extension Algorithm where A: Equatable { public func evaluate(recur: (Term, Term) -> Diff) -> B { return evaluate(==, recur: recur) } @@ -126,8 +126,8 @@ extension Free: FreeConvertible { public var free: Free { return self } } -extension FreeAlgorithm where B: FreeConvertible, B.RollType == A, B.PureType == Patch { - /// `FreeAlgorithm`s can be constructed from a pair of `Term`s using `ByKey` when `Keyed`, `ByIndex` when `Indexed`, and `Recursive` otherwise. +extension Algorithm where B: FreeConvertible, B.RollType == A, B.PureType == Patch { + /// `Algorithm`s can be constructed from a pair of `Term`s using `ByKey` when `Keyed`, `ByIndex` when `Indexed`, and `Recursive` otherwise. public init(_ a: Term, _ b: Term) { switch (a.out, b.out) { case let (.Keyed(a), .Keyed(b)): @@ -135,16 +135,16 @@ extension FreeAlgorithm where B: FreeConvertible, B.RollType == A, B.PureType == case let (.Indexed(a), .Indexed(b)): self = .Roll(.ByIndex(a, b, Syntax.Indexed >>> Free.Roll >>> B.init >>> Pure)) default: - self = .Roll(.Recursive(a, b, B.init >>> FreeAlgorithm.Pure)) + self = .Roll(.Recursive(a, b, B.init >>> Algorithm.Pure)) } } public func evaluate(equals: (A, A) -> Bool) -> B { - return evaluate(equals, recur: { FreeAlgorithm($0, $1).evaluate(equals).free }) + return evaluate(equals, recur: { Algorithm($0, $1).evaluate(equals).free }) } } -extension FreeAlgorithm where A: Equatable, B: FreeConvertible, B.RollType == A, B.PureType == Patch { +extension Algorithm where A: Equatable, B: FreeConvertible, B.RollType == A, B.PureType == Patch { public func evaluate() -> B { return evaluate(==) } From 10ddaab0ed2f707c4e04c9c6cad25b82bd3c6e7e Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:04:03 -0400 Subject: [PATCH 05/14] Use `values` instead of mapping. --- prototype/Doubt/SES.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prototype/Doubt/SES.swift b/prototype/Doubt/SES.swift index 3b689ef09..c4566c4e7 100644 --- a/prototype/Doubt/SES.swift +++ b/prototype/Doubt/SES.swift @@ -18,7 +18,7 @@ public func SES(a: [Fix], _ b: [Fix], equals: (A, A) -> Bool, recur: (F case let .Indexed(costs): return costs.reduce(0, combine: +) case let .Keyed(costs): - return costs.lazy.map { $1 }.reduce(0, combine: +) + return costs.values.reduce(0, combine: +) } } } From 47dec5546d7f7343b820c679940ea3ca47cc0ed5 Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:09:22 -0400 Subject: [PATCH 06/14] Restore the compare-parametric `min` function. --- prototype/Doubt/SES.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/prototype/Doubt/SES.swift b/prototype/Doubt/SES.swift index c4566c4e7..2c00a7805 100644 --- a/prototype/Doubt/SES.swift +++ b/prototype/Doubt/SES.swift @@ -31,6 +31,10 @@ public func SES(a: [Fix], _ b: [Fix], equals: (A, A) -> Bool, recur: (F return stream.value.first?.1 ?? 0 } + func min(a: A, _ rest: A..., _ isLessThan: (A, A) -> Bool) -> A { + return rest.reduce(a, combine: { isLessThan($0, $1) ? $0 : $1 }) + } + // A matrix whose values are streams representing paths through the edit graph, carrying both the diff & the cost of the remainder of the path. var matrix: Matrix>! matrix = Matrix(width: a.count + 1, height: b.count + 1) { i, j in From c2b00c2230fe40cbabb9a7fda74fc15bdf3c77ac Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:13:32 -0400 Subject: [PATCH 07/14] Nominate the best interior edge using compare-parametric min. --- prototype/Doubt/SES.swift | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/prototype/Doubt/SES.swift b/prototype/Doubt/SES.swift index 2c00a7805..bcdbaa9cd 100644 --- a/prototype/Doubt/SES.swift +++ b/prototype/Doubt/SES.swift @@ -55,19 +55,11 @@ public func SES(a: [Fix], _ b: [Fix], equals: (A, A) -> Bool, recur: (F } if let right = right, down = down, diagonal = diagonal { - let costs = (right: costOfStream(right), down: costOfStream(down), diagonal: costOfStream(diagonal)) // nominate the best edge to continue along - let best: Memo> - let diff: Diff - if costs.diagonal < costs.down { - (best, diff) = costs.diagonal < costs.right - ? (diagonal, recur(a[i], b[j])) - : (right, Diff.Pure(Patch.Delete(a[i]))) - } else { - (best, diff) = costs.down < costs.right - ? (down, Diff.Pure(Patch.Insert(b[j]))) - : (right, Diff.Pure(Patch.Delete(a[i]))) - } + let (best, diff, _) = min( + (diagonal, recur(a[i], b[j]), costOfStream(diagonal)), + (right, Diff.Pure(Patch.Delete(a[i])), costOfStream(right)), + (down, Diff.Pure(Patch.Insert(b[j])), costOfStream(down))) { $0.2 < $1.2 } return cons(diff, rest: best) } From 4bcc773fc6f44f2611ed279a419abae5541d8732 Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:13:51 -0400 Subject: [PATCH 08/14] :fire: chaff. --- prototype/Doubt/SES.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/prototype/Doubt/SES.swift b/prototype/Doubt/SES.swift index bcdbaa9cd..870776966 100644 --- a/prototype/Doubt/SES.swift +++ b/prototype/Doubt/SES.swift @@ -1,5 +1,3 @@ -// Copyright © 2015 GitHub. All rights reserved. - /// Computes the SES (shortest edit script), i.e. the shortest sequence of diffs (`Free>`) for two arrays of terms (`Fix`) which would suffice to transform `a` into `b`. /// /// This is computed w.r.t. an `equals` function, which computes the equality of leaf nodes within terms, and a `recur` function, which produces diffs representing matched-up terms. From ba650f8080f9acf087b504d6e76acb307d6eeb1a Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:19:49 -0400 Subject: [PATCH 09/14] Handle the zero-category case with a where clause. --- prototype/Doubt/Info.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/prototype/Doubt/Info.swift b/prototype/Doubt/Info.swift index 425f87103..96212e05c 100644 --- a/prototype/Doubt/Info.swift +++ b/prototype/Doubt/Info.swift @@ -18,10 +18,10 @@ public enum Info: AlgebraicHashable, CustomDebugStringConvertible { public var debugDescription: String { switch self { + case let .Literal(s, c) where c.isEmpty: + return s case let .Literal(s, c): - return c.isEmpty - ? s - : s + " (" + c.map { String(reflecting: $0) }.joinWithSeparator(", ") + ")" + return s + " (" + c.map { String(reflecting: $0) }.joinWithSeparator(", ") + ")" } } } From aef4c3275477707083a45b1d17898a62a3780f37 Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:20:09 -0400 Subject: [PATCH 10/14] =?UTF-8?q?Don=E2=80=99t=20bother=20destructuring=20?= =?UTF-8?q?tags.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prototype/Doubt/Category.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/prototype/Doubt/Category.swift b/prototype/Doubt/Category.swift index 7b78c9ed4..83c399ca6 100644 --- a/prototype/Doubt/Category.swift +++ b/prototype/Doubt/Category.swift @@ -25,8 +25,5 @@ public enum Category: AlgebraicHashable, CustomDebugStringConvertible { } public func == (left: Category, right: Category) -> Bool { - switch (left, right) { - case let (.Tag(a), .Tag(b)): - return a == b - } + return left.tag == right.tag } From f8b89bd93b702489dc67f55237be50318d421e8c Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:20:22 -0400 Subject: [PATCH 11/14] Category is Comparable. --- prototype/Doubt/Category.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/prototype/Doubt/Category.swift b/prototype/Doubt/Category.swift index 83c399ca6..4cd253629 100644 --- a/prototype/Doubt/Category.swift +++ b/prototype/Doubt/Category.swift @@ -1,4 +1,4 @@ -public enum Category: AlgebraicHashable, CustomDebugStringConvertible { +public enum Category: AlgebraicHashable, Comparable, CustomDebugStringConvertible { case Tag(String) @@ -27,3 +27,7 @@ public enum Category: AlgebraicHashable, CustomDebugStringConvertible { public func == (left: Category, right: Category) -> Bool { return left.tag == right.tag } + +public func < (left: Category, right: Category) -> Bool { + return left.tag < right.tag +} From 21ac5822a00373d911522526d18f108edb69b2c7 Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:20:41 -0400 Subject: [PATCH 12/14] Sort categories alphabetically. --- prototype/Doubt/Info.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prototype/Doubt/Info.swift b/prototype/Doubt/Info.swift index 96212e05c..81a68cf1e 100644 --- a/prototype/Doubt/Info.swift +++ b/prototype/Doubt/Info.swift @@ -21,7 +21,7 @@ public enum Info: AlgebraicHashable, CustomDebugStringConvertible { case let .Literal(s, c) where c.isEmpty: return s case let .Literal(s, c): - return s + " (" + c.map { String(reflecting: $0) }.joinWithSeparator(", ") + ")" + return s + " (" + c.sort().map { String(reflecting: $0) }.joinWithSeparator(", ") + ")" } } } From 06c456f1cdac956c55662f346f1e8662a3cc3bd1 Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:25:02 -0400 Subject: [PATCH 13/14] Document dictionary diffing as an approach stemming from set reconciliation. --- prototype/Doubt/Algorithm.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/prototype/Doubt/Algorithm.swift b/prototype/Doubt/Algorithm.swift index 3a28e1d8c..e79124e5a 100644 --- a/prototype/Doubt/Algorithm.swift +++ b/prototype/Doubt/Algorithm.swift @@ -95,6 +95,7 @@ public enum Algorithm { ? Diff($1) : recur($0, $1) } + // Essentially [set reconciliation](https://en.wikipedia.org/wiki/Data_synchronization#Unordered_data) on the keys, followed by recurring into the values of the intersecting keys. let deleted = Set(a.keys).subtract(b.keys).map { ($0, Diff.Pure(Patch.Delete(a[$0]!))) } let inserted = Set(b.keys).subtract(a.keys).map { ($0, Diff.Pure(Patch.Insert(b[$0]!))) } let patched = Set(a.keys).intersect(b.keys).map { ($0, recur(a[$0]!, b[$0]!)) } From 0ad285fb26f1dc521dd7a85a5ae942674549ed05 Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Wed, 7 Oct 2015 13:28:16 -0400 Subject: [PATCH 14/14] Document the Recursive operation a little. --- prototype/Doubt/Algorithm.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prototype/Doubt/Algorithm.swift b/prototype/Doubt/Algorithm.swift index e79124e5a..6774f7eed 100644 --- a/prototype/Doubt/Algorithm.swift +++ b/prototype/Doubt/Algorithm.swift @@ -84,6 +84,9 @@ public enum Algorithm { return b case let .Roll(.Recursive(a, b, f)): + // Recur structurally into both terms, if compatible, patching paired sub-terms. This is akin to the shape of unification, except that it computes a patched tree instead of a substitution. It’s also a little like a structural zip on the pair of terms. + // + // At the moment, there are no restrictions on whether terms are compatible, and there is no structure to exploit in terms; therefore, this simplifies to copying if equal, and replacing otherwise. return f(Term.equals(equals)(a, b) ? Diff(b) // This must not call `recur` with `a` and `b`, as that would infinite loop if actually recursive.