1
1
mirror of https://github.com/github/semantic.git synced 2024-11-25 11:04:00 +03:00

Merge branch 'generalize-patch' into diff-cofree-values

This commit is contained in:
Rob Rix 2015-10-14 13:31:00 -04:00
commit 31d905ac64
7 changed files with 100 additions and 97 deletions

View File

@ -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 = "<group>"; };
D42F097D1BCEAEDA00B95610 /* Operation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
D432D4701BA9AC0B00F3FABC /* SESTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SESTests.swift; sourceTree = "<group>"; };
D435B7521BB31BBC000902F6 /* BoundsCheckedArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoundsCheckedArray.swift; sourceTree = "<group>"; };
D4413FEE1BB06D4C00E3C3C1 /* Dictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -1,38 +1,3 @@
/// An operation of diffing over terms or collections of terms.
public enum Operation<Recur, A> {
/// The type of `Term`s over which `Operation`s operate.
public typealias Term = Fix<A>
/// The type of `Diff`s which `Operation`s produce.
public typealias Diff = Free<A, Patch<A>>
/// 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<Other>(transform: Recur -> Other) -> Operation<Other, A> {
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<Recur, A> {
/// 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<A, B> {
/// The type of `Term`s over which `Algorithm`s operate.
public typealias Term = Operation<Algorithm, A>.Term
public typealias Term = Fix<A>
/// The type of `Patch`es produced by `Algorithm`s.
public typealias Patch = Doubt.Patch<Term>
/// The type of `Diff`s which `Algorithm`s produce.
public typealias Diff = Operation<Algorithm, A>.Diff
public typealias Diff = Free<A, Patch>
/// The injection of a value of type `B` into an `Operation`.
///
@ -51,9 +19,9 @@ public enum Algorithm<A, B> {
case Pure(B)
/// A recursive instantiation of `Operation`, unrolling another iteration of the recursive type.
case Roll(Operation<Algorithm, A>)
case Roll(Operation<Algorithm, Term, Diff>)
public func analysis<C>(@noescape ifPure ifPure: B -> C, @noescape ifRoll: Operation<Algorithm, A> -> C) -> C {
public func analysis<C>(@noescape ifPure ifPure: B -> C, @noescape ifRoll: Operation<Algorithm, Term, Diff> -> C) -> C {
switch self {
case let .Pure(b):
return ifPure(b)
@ -78,7 +46,15 @@ public enum Algorithm<A, B> {
/// 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<A, B> {
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<A, B> {
}
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<A> {
extension Algorithm where B: FreeConvertible, B.RollType == A, B.PureType == Algorithm<A, B>.Patch {
/// `Algorithm<A, Diff>`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<A> {
extension Algorithm where A: Equatable, B: FreeConvertible, B.RollType == A, B.PureType == Algorithm<A, B>.Patch {
public func evaluate() -> B {
return evaluate(==)
}

View File

@ -107,7 +107,7 @@ public enum Free<A, B>: CustomDebugStringConvertible, CustomDocConvertible, Synt
}
extension Free where B: PatchConvertible, B.Info == A {
extension Free where B: PatchConvertible, B.Element == Fix<A> {
public typealias Term = Fix<A>
private func discardNullTerms(syntax: Syntax<Term?, A>) -> 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<A> {
public var JSON: Doubt.JSON {
return JSON { $0.patch.JSON }
return JSON { $0.patch.JSON { $0.JSON } }
}
}

View File

@ -0,0 +1,32 @@
/// An operation of diffing over terms or collections of terms.
public enum Operation<Recur, Term, Diff> {
/// 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<Other>(transform: Recur -> Other) -> Operation<Other, Term, Diff> {
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

View File

@ -1,10 +1,10 @@
/// A patch to some part of a `Syntax` tree.
public enum Patch<A>: CustomDebugStringConvertible, CustomDocConvertible {
case Replace(Fix<A>, Fix<A>)
case Insert(Fix<A>)
case Delete(Fix<A>)
case Replace(A, A)
case Insert(A)
case Delete(A)
public var state: (before: Fix<A>?, after: Fix<A>?) {
public var state: (before: A?, after: A?) {
switch self {
case let .Replace(a, b):
return (a, b)
@ -56,8 +56,8 @@ public enum Patch<A>: 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<A>: 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, Patch<A>>`.
/// A hack to enable constrained extensions on `Free<A, Patch<Fix<A>>`.
public protocol PatchConvertible {
typealias Info
typealias Element
init(patch: Patch<Info>)
var patch: Patch<Info> { get }
init(patch: Patch<Element>)
var patch: Patch<Element> { get }
}
extension Patch: PatchConvertible {

View File

@ -1,9 +1,8 @@
/// Computes the SES (shortest edit script), i.e. the shortest sequence of diffs (`Free<A, Patch<A>>`) for two arrays of terms (`Fix<A>`) which would suffice to transform `a` into `b`.
/// Computes the SES (shortest edit script), i.e. the shortest sequence of diffs (`Free<A, Patch<Term>>`) 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>(a: [Fix<A>], _ b: [Fix<A>], equals: (A, A) -> Bool, recur: (Fix<A>, Fix<A>) -> Free<A, Patch<A>>) -> [Free<A, Patch<A>>] {
typealias Term = Fix<A>
typealias Diff = Free<A, Patch<A>>
public func SES<Term, A>(a: [Term], _ b: [Term], recur: (Term, Term) -> Free<A, Patch<Term>>?) -> [Free<A, Patch<Term>>] {
typealias Diff = Free<A, Patch<Term>>
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>(a: [Fix<A>], _ b: [Fix<A>], 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)
}

View File

@ -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<Info>
private typealias Diff = Free<Info, Patch<Info>>
private typealias Term = Fix<String>
private typealias Diff = Free<String, Patch<Term>>
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