diff --git a/prototype/Doubt.xcodeproj/project.pbxproj b/prototype/Doubt.xcodeproj/project.pbxproj index 03c266567..d73f04aef 100644 --- a/prototype/Doubt.xcodeproj/project.pbxproj +++ b/prototype/Doubt.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ D42F09791BCCC5DC00B95610 /* Prelude.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D42F09691BCCC41600B95610 /* Prelude.framework */; }; D42F097A1BCCC5DC00B95610 /* Stream.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D42F096A1BCCC41600B95610 /* Stream.framework */; }; D42F097C1BCE914A00B95610 /* Cofree.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42F097B1BCE914A00B95610 /* Cofree.swift */; settings = {ASSET_TAGS = (); }; }; + D42F097E1BCEAEDA00B95610 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42F097D1BCEAEDA00B95610 /* Operation.swift */; settings = {ASSET_TAGS = (); }; }; D432D4711BA9AC0B00F3FABC /* SESTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D432D4701BA9AC0B00F3FABC /* SESTests.swift */; }; D4413FEF1BB06D4C00E3C3C1 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4413FEE1BB06D4C00E3C3C1 /* Dictionary.swift */; }; D4413FF11BB08FDC00E3C3C1 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4413FF01BB08FDC00E3C3C1 /* JSON.swift */; }; @@ -64,6 +65,7 @@ D42F09691BCCC41600B95610 /* Prelude.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Prelude.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D42F096A1BCCC41600B95610 /* Stream.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Stream.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D42F097B1BCE914A00B95610 /* Cofree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cofree.swift; sourceTree = ""; }; + D42F097D1BCEAEDA00B95610 /* Operation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; D432D4701BA9AC0B00F3FABC /* SESTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SESTests.swift; sourceTree = ""; }; D435B7521BB31BBC000902F6 /* BoundsCheckedArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoundsCheckedArray.swift; sourceTree = ""; }; D4413FEE1BB06D4C00E3C3C1 /* Dictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; @@ -172,6 +174,7 @@ D42F097B1BCE914A00B95610 /* Cofree.swift */, D49FCBC51BBF214300C5E9C3 /* Patch.swift */, D49FCBC71BBF2C4300C5E9C3 /* Algorithm.swift */, + D42F097D1BCEAEDA00B95610 /* Operation.swift */, D4DF96EC1BC46B630040F41F /* SES.swift */, D435B7521BB31BBC000902F6 /* BoundsCheckedArray.swift */, D4AAE5001B5AE22E004E581F /* Supporting Files */, @@ -350,6 +353,7 @@ D4AAE5471B5AE2D0004E581F /* Optional.swift in Sources */, D4413FEF1BB06D4C00E3C3C1 /* Dictionary.swift in Sources */, D4D7F3171BBB22E500AAB0C0 /* Hash.swift in Sources */, + D42F097E1BCEAEDA00B95610 /* Operation.swift in Sources */, D45A36C91BBC667D00BE3DDE /* Category.swift in Sources */, D4DF96ED1BC46B630040F41F /* SES.swift in Sources */, D4AAE5401B5AE2D0004E581F /* RangeReplaceableCollectionType.swift in Sources */, diff --git a/prototype/Doubt/Algorithm.swift b/prototype/Doubt/Algorithm.swift index fc9aa4041..56dab9f97 100644 --- a/prototype/Doubt/Algorithm.swift +++ b/prototype/Doubt/Algorithm.swift @@ -1,38 +1,3 @@ -/// An operation of diffing over terms or collections of terms. -public enum Operation { - /// The type of `Term`s over which `Operation`s operate. - public typealias Term = Fix - - /// The type of `Diff`s which `Operation`s produce. - public typealias Diff = Free> - - /// Indicates that diffing should compare the enclosed `Term`s. - /// - /// When run, the enclosed function will be applied to the resulting `Diff`. - case Recursive(Term, Term, Diff -> Recur) - - /// Represents a diff to be performed on a collection of terms identified by keys. - case ByKey([String:Term], [String:Term], [String:Diff] -> Recur) - - /// Represents a diff to be performed over an array of terms by index. - case ByIndex([Term], [Term], [Diff] -> Recur) - - - // MARK: Functor - - public func map(transform: Recur -> Other) -> Operation { - switch self { - case let .Recursive(a, b, f): - return .Recursive(a, b, f >>> transform) - case let .ByKey(a, b, f): - return .ByKey(a, b, f >>> transform) - case let .ByIndex(a, b, f): - return .ByIndex(a, b, f >>> transform) - } - } -} - - /// 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 `Operation` without extra assumptions.” @@ -40,10 +5,13 @@ public enum Operation { /// 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 + public typealias Term = Fix + + /// The type of `Patch`es produced by `Algorithm`s. + public typealias Patch = Doubt.Patch /// The type of `Diff`s which `Algorithm`s produce. - public typealias Diff = Operation.Diff + public typealias Diff = Free /// The injection of a value of type `B` into an `Operation`. /// @@ -51,9 +19,9 @@ public enum Algorithm { 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) @@ -78,7 +46,15 @@ public enum Algorithm { /// Evaluates the encoded algorithm, returning its result. - public func evaluate(equals: (A, A) -> Bool, recur: (Term, Term) -> Diff) -> B { + public func evaluate(equals: (A, A) -> Bool, recur: (Term, Term) -> Diff?) -> B { + let recur = { + Term.equals(equals)($0, $1) + ? Diff($1) + : recur($0, $1) + } + let recurOrReplace = { + recur($0, $1) ?? .Pure(.Replace($0, $1)) + } switch self { case let .Pure(b): return b @@ -91,10 +67,10 @@ public enum Algorithm { switch (a.out, b.out) { case let (.Indexed(a), .Indexed(b)) where a.count == b.count: - return f(.Indexed(zip(a, b).map(recur))).evaluate(equals, recur: recur) + return f(.Indexed(zip(a, b).map(recurOrReplace))).evaluate(equals, recur: recur) case let (.Keyed(a), .Keyed(b)) where Array(a.keys) == Array(b.keys): - return f(.Keyed(Dictionary(elements: b.keys.map { ($0, recur(a[$0]!, b[$0]!)) }))).evaluate(equals, recur: recur) + return f(.Keyed(Dictionary(elements: b.keys.map { ($0, recurOrReplace(a[$0]!, b[$0]!)) }))).evaluate(equals, recur: recur) default: // This must not call `recur` with `a` and `b`, as that would infinite loop if actually recursive. @@ -102,25 +78,20 @@ public enum Algorithm { } case let .Roll(.ByKey(a, b, f)): - let recur = { - Term.equals(equals)($0, $1) - ? 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]!)) } + let patched = Set(a.keys).intersect(b.keys).map { ($0, recurOrReplace(a[$0]!, b[$0]!)) } return f(Dictionary(elements: deleted + inserted + patched)).evaluate(equals, recur: recur) case let .Roll(.ByIndex(a, b, f)): - return f(SES(a, b, equals: equals, recur: recur)).evaluate(equals, recur: recur) + return f(SES(a, b, recur: recur)).evaluate(equals, recur: recur) } } } extension Algorithm where A: Equatable { - public func evaluate(recur: (Term, Term) -> Diff) -> B { + public func evaluate(recur: (Term, Term) -> Diff?) -> B { return evaluate(==, recur: recur) } } @@ -139,7 +110,7 @@ extension Free: FreeConvertible { public var free: Free { return self } } -extension Algorithm where B: FreeConvertible, B.RollType == A, B.PureType == Patch { +extension Algorithm where B: FreeConvertible, B.RollType == A, B.PureType == Algorithm.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) { @@ -157,7 +128,7 @@ extension Algorithm where B: FreeConvertible, B.RollType == A, B.PureType == Pat } } -extension Algorithm where A: Equatable, B: FreeConvertible, B.RollType == A, B.PureType == Patch { +extension Algorithm where A: Equatable, B: FreeConvertible, B.RollType == A, B.PureType == Algorithm.Patch { public func evaluate() -> B { return evaluate(==) } diff --git a/prototype/Doubt/Free.swift b/prototype/Doubt/Free.swift index 467770eee..35dc108b4 100644 --- a/prototype/Doubt/Free.swift +++ b/prototype/Doubt/Free.swift @@ -107,7 +107,7 @@ public enum Free: CustomDebugStringConvertible, CustomDocConvertible, Synt } -extension Free where B: PatchConvertible, B.Info == A { +extension Free where B: PatchConvertible, B.Element == Fix { public typealias Term = Fix private func discardNullTerms(syntax: Syntax) -> Term? { @@ -190,9 +190,9 @@ extension Free where A: CustomJSONConvertible { } } -extension Free where A: CustomJSONConvertible, B: PatchConvertible, B.Info == A { +extension Free where A: CustomJSONConvertible, B: PatchConvertible, B.Element == Fix { public var JSON: Doubt.JSON { - return JSON { $0.patch.JSON } + return JSON { $0.patch.JSON { $0.JSON } } } } diff --git a/prototype/Doubt/Operation.swift b/prototype/Doubt/Operation.swift new file mode 100644 index 000000000..b29d5fbb3 --- /dev/null +++ b/prototype/Doubt/Operation.swift @@ -0,0 +1,32 @@ +/// An operation of diffing over terms or collections of terms. +public enum Operation { + /// Indicates that diffing should compare the enclosed `Term`s. + /// + /// When run, the enclosed function will be applied to the resulting `Diff`. + case Recursive(Term, Term, Diff -> Recur) + + /// Represents a diff to be performed on a collection of terms identified by keys. + case ByKey([String:Term], [String:Term], [String:Diff] -> Recur) + + /// Represents a diff to be performed over an array of terms by index. + case ByIndex([Term], [Term], [Diff] -> Recur) +} + + +// MARK: - Functor + +extension Operation { + public func map(transform: Recur -> Other) -> Operation { + switch self { + case let .Recursive(a, b, f): + return .Recursive(a, b, f >>> transform) + case let .ByKey(a, b, f): + return .ByKey(a, b, f >>> transform) + case let .ByIndex(a, b, f): + return .ByIndex(a, b, f >>> transform) + } + } +} + + +import Prelude diff --git a/prototype/Doubt/Patch.swift b/prototype/Doubt/Patch.swift index 7c87903f6..057694d1b 100644 --- a/prototype/Doubt/Patch.swift +++ b/prototype/Doubt/Patch.swift @@ -1,10 +1,10 @@ /// A patch to some part of a `Syntax` tree. public enum Patch: CustomDebugStringConvertible, CustomDocConvertible { - case Replace(Fix, Fix) - case Insert(Fix) - case Delete(Fix) + case Replace(A, A) + case Insert(A) + case Delete(A) - public var state: (before: Fix?, after: Fix?) { + public var state: (before: A?, after: A?) { switch self { case let .Replace(a, b): return (a, b) @@ -56,8 +56,8 @@ public enum Patch: CustomDebugStringConvertible, CustomDocConvertible { // MARK: CustomDocConvertible public var doc: Doc { - return (state.before?.doc.bracket("{-", "-}") ?? .Empty) - <> (state.after?.doc.bracket("{+", "+}") ?? .Empty) + return (state.before.map(Doc.init)?.bracket("{-", "-}") ?? .Empty) + <> (state.after.map(Doc.init)?.bracket("{+", "+}") ?? .Empty) } } @@ -66,8 +66,8 @@ public enum Patch: CustomDebugStringConvertible, CustomDocConvertible { extension Patch { public static func equals(param: (A, A) -> Bool)(_ left: Patch, _ right: Patch) -> Bool { - return Optional.equals(Fix.equals(param))(left.state.before, right.state.before) - && Optional.equals(Fix.equals(param))(left.state.after, right.state.after) + return Optional.equals(param)(left.state.before, right.state.before) + && Optional.equals(param)(left.state.after, right.state.after) } } @@ -77,8 +77,8 @@ extension Patch { extension Patch { public func hash(param: A -> Hash) -> Hash { return Hash.Ordered([ - state.before.map { $0.hash(param) } ?? Hash.Empty, - state.after.map { $0.hash(param) } ?? Hash.Empty + state.before.map(param) ?? Hash.Empty, + state.after.map(param) ?? Hash.Empty ]) } } @@ -92,17 +92,17 @@ extension Patch { case let .Replace(a, b): return [ "replace": [ - "before": a.JSON(ifLeaf), - "after": b.JSON(ifLeaf), + "before": ifLeaf(a), + "after": ifLeaf(b), ] ] case let .Insert(b): return [ - "insert": b.JSON(ifLeaf), + "insert": ifLeaf(b), ] case let .Delete(a): return [ - "delete": a.JSON(ifLeaf) + "delete": ifLeaf(a) ] } } @@ -111,17 +111,17 @@ extension Patch { extension Patch where A: CustomJSONConvertible { public var JSON: Doubt.JSON { - return self.JSON { $0.JSON } + return JSON { $0.JSON } } } -/// A hack to enable constrained extensions on `Free>`. +/// A hack to enable constrained extensions on `Free>`. public protocol PatchConvertible { - typealias Info + typealias Element - init(patch: Patch) - var patch: Patch { get } + init(patch: Patch) + var patch: Patch { get } } extension Patch: PatchConvertible { diff --git a/prototype/Doubt/SES.swift b/prototype/Doubt/SES.swift index 57370a53d..847b84434 100644 --- a/prototype/Doubt/SES.swift +++ b/prototype/Doubt/SES.swift @@ -1,9 +1,8 @@ -/// 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`. +/// Computes the SES (shortest edit script), i.e. the shortest sequence of diffs (`Free>`) for two arrays of `Term`s 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. -public func SES(a: [Fix], _ b: [Fix], equals: (A, A) -> Bool, recur: (Fix, Fix) -> Free>) -> [Free>] { - typealias Term = Fix - typealias Diff = Free> +public func SES(a: [Term], _ b: [Term], recur: (Term, Term) -> Free>?) -> [Free>] { + typealias Diff = Free> if a.isEmpty { return b.map { Diff.Pure(Patch.Insert($0)) } } if b.isEmpty { return a.map { Diff.Pure(Patch.Delete($0)) } } @@ -46,18 +45,14 @@ public func SES(a: [Fix], _ b: [Fix], equals: (A, A) -> Bool, recur: (F let down = matrix[i, j + 1] let diagonal = matrix[i + 1, j + 1] - let recur = { - Term.equals(equals)($0, $1) - ? Diff($1) - : recur($0, $1) - } - if let right = right, down = down, diagonal = diagonal { - // nominate the best edge to continue along - 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 } + let right = (right, Diff.Pure(Patch.Delete(a[i])), costOfStream(right)) + let down = (down, Diff.Pure(Patch.Insert(b[j])), costOfStream(down)) + let diagonal = recur(a[i], b[j]).map { (diagonal, $0, costOfStream(diagonal)) } + // nominate the best edge to continue along, not considering diagonal if `recur` returned `nil`. + let (best, diff, _) = diagonal + .map { min($0, right, down) { $0.2 < $1.2 } } + ?? min(right, down) { $0.2 < $1.2 } return cons(diff, rest: best) } diff --git a/prototype/DoubtTests/SESTests.swift b/prototype/DoubtTests/SESTests.swift index 431881e85..641c366b5 100644 --- a/prototype/DoubtTests/SESTests.swift +++ b/prototype/DoubtTests/SESTests.swift @@ -36,7 +36,7 @@ final class SESTests: XCTestCase { } func testSESOfLongerSequences() { - assert(SES([ a, b, c, a, b, b, a ], [ c, b, a, b, a, c ]), ==, [ Diff.Pure(.Replace(a, c)), Diff(b), delete(c), Diff(a), delete(b), Diff(b), Diff(a), insert(c) ]) + assert(SES([ a, b, c, a, b, b, a ], [ c, b, a, b, a, c ]), ==, [ insert(c), delete(a), Diff(b), delete(c), Diff(a), delete(b), Diff(b), Diff(a), insert(c) ]) } } @@ -48,16 +48,16 @@ private func delete(term: Term) -> Diff { return Diff.Pure(.Delete(term)) } -private typealias Term = Fix -private typealias Diff = Free> +private typealias Term = Fix +private typealias Diff = Free> -private let a = Term.Leaf(.Literal("a", [])) -private let b = Term.Leaf(.Literal("b", [])) -private let c = Term.Leaf(.Literal("c", [])) -private let d = Term.Leaf(.Literal("d", [])) +private let a = Term.Leaf("a") +private let b = Term.Leaf("b") +private let c = Term.Leaf("c") +private let d = Term.Leaf("d") private func SES(a: [Term], _ b: [Term]) -> [Diff] { - return SES(a, b, equals: ==, recur: { Diff.Pure(Patch.Replace($0, $1)) }) + return SES(a, b) { $0 == $1 ? Diff($1) : nil } } private func == (a: [Diff], b: [Diff]) -> Bool { @@ -67,4 +67,5 @@ private func == (a: [Diff], b: [Diff]) -> Bool { import Assertions @testable import Doubt +import Prelude import XCTest