1
1
mirror of https://github.com/github/semantic.git synced 2024-11-29 02:44:36 +03:00

Merge pull request #143 from github/annotate-diffs

Annotate diffs
This commit is contained in:
Josh Vera 2015-10-23 12:01:26 -04:00
commit 829e43a168
15 changed files with 232 additions and 180 deletions

View File

@ -3,12 +3,12 @@
/// As with `Free`, this is free in the sense of unconstrained, i.e. the monad induced by `Operation` 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 `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 `Result` 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, `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 `Result` 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<Term: TermType, Result> { public enum Algorithm<Term: CofreeType, Result> {
/// The type of `Patch`es produced by `Algorithm`s. /// The type of `Patch`es produced by `Algorithm`s.
public typealias Patch = Doubt.Patch<Term> public typealias Patch = Doubt.Patch<Term>
/// The type of `Diff`s which `Algorithm`s produce. /// The type of `Diff`s which `Algorithm`s produce.
public typealias Diff = Free<Term.LeafType, Patch> public typealias Diff = Free<Term.Leaf, (Term.Annotation, Term.Annotation), Patch>
/// The injection of a value of type `Result` into an `Operation`. /// The injection of a value of type `Result` into an `Operation`.
/// ///

View File

@ -1,12 +1,12 @@
/// The cofree comonad over `Syntax`. /// The cofree comonad over `Syntax`.
/// ///
/// This is free in the sense of unconstrained rather than zero-cost; its the comonad obtained by taking a functor (in this case `Syntax`) and adding the minimum necessary details (the `B` paired with it) to satisfy the comonad laws. /// This is free in the sense of unconstrained rather than zero-cost; its the comonad obtained by taking a functor (in this case `Syntax`) and adding the minimum necessary details (the `Annotation` paired with it) to satisfy the comonad laws.
/// ///
/// This type is dual to `Free`. Where `Free` is inhabited by syntax trees where some terms are replaced with `B`s, `Cofree` is inhabited by syntax trees where all terms are annotated with `B`s. In Doubt, this allows us to e.g. annotate terms with source range information, categorization, etc. /// This type is dual to `Free`. Where `Free` is inhabited by syntax trees where some terms are replaced with `Annotation`s, `Cofree` is inhabited by syntax trees where all terms are annotated with `Annotation`s. In Doubt, this allows us to e.g. annotate terms with source range information, categorization, etc.
public enum Cofree<A, B> { public enum Cofree<Leaf, Annotation> {
indirect case Unroll(B, Syntax<Cofree, A>) indirect case Unroll(Annotation, Syntax<Cofree, Leaf>)
public var unwrap: Syntax<Cofree, A> { public var unwrap: Syntax<Cofree, Leaf> {
switch self { switch self {
case let .Unroll(_, rest): case let .Unroll(_, rest):
return rest return rest
@ -14,19 +14,9 @@ public enum Cofree<A, B> {
} }
public init(_ annotation: B, _ syntax: Syntax<Cofree, A>) { public init(_ annotation: Annotation, _ syntax: Syntax<Cofree, Leaf>) {
self = .Unroll(annotation, syntax) self = .Unroll(annotation, syntax)
} }
/// Constructs a cofree by coiteration.
///
/// This is an _anamorphism_ (from the Greek ana, upwards; compare anabolism), a generalization of unfolds over regular trees (and datatypes isomorphic to them). The initial seed is used as the annotation of the returned value. The continuation of the structure is unpacked by applying `annotate` to the seed and mapping the resulting syntaxs values recursively. In this manner, the structure is unfolded bottom-up, starting with `seed` and ending at the leaves.
///
/// As this is the dual of `Free.iterate`, its unsurprising that we have a similar guarantee: coiteration is linear in the size of the constructed tree.
public static func coiterate(annotate: B -> Syntax<B, A>)(_ seed: B) -> Cofree {
return .Unroll(seed, annotate(seed).map(coiterate(annotate)))
}
} }
@ -42,7 +32,7 @@ extension Cofree: CustomDebugStringConvertible {
// MARK: - Functor // MARK: - Functor
extension Cofree { extension Cofree {
public func map<Other>(transform: B -> Other) -> Cofree<A, Other> { public func map<Other>(transform: Annotation -> Other) -> Cofree<Leaf, Other> {
return .Unroll(transform(extract), unwrap.map { $0.map(transform) }) return .Unroll(transform(extract), unwrap.map { $0.map(transform) })
} }
} }
@ -52,7 +42,7 @@ extension Cofree {
extension Cofree { extension Cofree {
/// Returns the value annotating the syntax tree at this node. /// Returns the value annotating the syntax tree at this node.
public var extract: B { public var extract: Annotation {
switch self { switch self {
case let .Unroll(b, _): case let .Unroll(b, _):
return b return b
@ -60,12 +50,12 @@ extension Cofree {
} }
/// Returns a new `Cofree` by recursively applying `transform` to each node, producing the annotations for the copy. /// Returns a new `Cofree` by recursively applying `transform` to each node, producing the annotations for the copy.
public func extend<Other>(transform: Cofree -> Other) -> Cofree<A, Other> { public func extend<Other>(transform: Cofree -> Other) -> Cofree<Leaf, Other> {
return .Unroll(transform(self), unwrap.map { $0.extend(transform) }) return .Unroll(transform(self), unwrap.map { $0.extend(transform) })
} }
/// Returns a new `Cofree` constructed by recursively annotating each subtree with itself. /// Returns a new `Cofree` constructed by recursively annotating each subtree with itself.
public var duplicate: Cofree<A, Cofree<A, B>> { public var duplicate: Cofree<Leaf, Cofree<Leaf, Annotation>> {
return extend(id) return extend(id)
} }
} }
@ -74,13 +64,13 @@ extension Cofree {
// MARK: - Equality // MARK: - Equality
extension Cofree { extension Cofree {
public static func equals(annotation annotation: (B, B) -> Bool, leaf: (A, A) -> Bool)(_ left: Cofree, _ right: Cofree) -> Bool { public static func equals(annotation annotation: (Annotation, Annotation) -> Bool, leaf: (Leaf, Leaf) -> Bool)(_ left: Cofree, _ right: Cofree) -> Bool {
return annotation(left.extract, right.extract) return annotation(left.extract, right.extract)
&& Syntax.equals(ifLeaf: leaf, ifRecur: Cofree.equals(annotation: annotation, leaf: leaf))(left.unwrap, right.unwrap) && Syntax.equals(leaf: leaf, recur: Cofree.equals(annotation: annotation, leaf: leaf))(left.unwrap, right.unwrap)
} }
} }
public func == <A: Equatable, B: Equatable> (left: Cofree<A, B>, right: Cofree<A, B>) -> Bool { public func == <Leaf: Equatable, Annotation: Equatable> (left: Cofree<Leaf, Annotation>, right: Cofree<Leaf, Annotation>) -> Bool {
return Cofree.equals(annotation: ==, leaf: ==)(left, right) return Cofree.equals(annotation: ==, leaf: ==)(left, right)
} }
@ -88,15 +78,15 @@ public func == <A: Equatable, B: Equatable> (left: Cofree<A, B>, right: Cofree<A
// MARK: - JSON // MARK: - JSON
extension Cofree { extension Cofree {
public func JSON(annotation annotation: B -> Doubt.JSON, leaf: A -> Doubt.JSON) -> Doubt.JSON { public func JSON(annotation annotation: Annotation -> Doubt.JSON, leaf: Leaf -> Doubt.JSON) -> Doubt.JSON {
return [ return [
"extract": annotation(extract), "extract": annotation(extract),
"unwrap": unwrap.JSON(ifLeaf: leaf, ifRecur: { $0.JSON(annotation: annotation, leaf: leaf) }) "unwrap": unwrap.JSON(leaf: leaf, recur: { $0.JSON(annotation: annotation, leaf: leaf) })
] ]
} }
} }
extension Cofree where A: CustomJSONConvertible, B: CustomJSONConvertible { extension Cofree where Leaf: CustomJSONConvertible, Annotation: CustomJSONConvertible {
public var JSON: Doubt.JSON { public var JSON: Doubt.JSON {
return JSON(annotation: { $0.JSON }, leaf: { $0.JSON }) return JSON(annotation: { $0.JSON }, leaf: { $0.JSON })
} }
@ -105,8 +95,8 @@ extension Cofree where A: CustomJSONConvertible, B: CustomJSONConvertible {
// MARK: - Categorizable // MARK: - Categorizable
extension Cofree where B: Categorizable { extension Cofree where Annotation: Categorizable {
var categories: Set<B.Category> { var categories: Set<Annotation.Category> {
return extract.categories return extract.categories
} }
} }
@ -114,12 +104,47 @@ extension Cofree where B: Categorizable {
// MARK: - CofreeType // MARK: - CofreeType
public protocol CofreeType { public protocol CofreeType: TermType {
typealias Annotation typealias Annotation
typealias Leaf
init(_ annotation: Annotation, _ syntax: Syntax<Self, Leaf>)
var extract: Annotation { get } var extract: Annotation { get }
var unwrap: Syntax<Self, Leaf> { get } }
extension CofreeType {
public static func Introduce(annotation: Annotation)(syntax: Syntax<Self, Leaf>) -> Self {
return Self(annotation, syntax)
}
public static func eliminate(term: Self) -> (Annotation, Syntax<Self, Leaf>) {
return (term.extract, term.unwrap)
}
/// Constructs a cofree by coiteration.
///
/// This is an _anamorphism_ (from the Greek ana, upwards; compare anabolism), a generalization of unfolds over regular trees (and datatypes isomorphic to them). The initial seed is used as the annotation of the returned value. The continuation of the structure is unpacked by applying `annotate` to the seed and mapping the resulting syntaxs values recursively. In this manner, the structure is unfolded bottom-up, starting with `seed` and ending at the leaves.
///
/// As this is the dual of `cata`, its unsurprising that we have a similar guarantee: coiteration is linear in the size of the constructed tree.
public static func ana(unfold: Annotation -> Syntax<Annotation, Leaf>)(_ seed: Annotation) -> Self {
return (Introduce(seed) <<< { $0.map(ana(unfold)) } <<< unfold) <| seed
}
/// `Zip` two `CofreeType` values into a single `Cofree`, pairing their annotations.
///
/// This is partial, returning `nil` for any pair of values which are not of the same shape, i.e. where they wrap `Syntax` values of different constructors. The values of leaves are always taken from the second parameter.
public static func zip(a: Self, _ b: Self) -> Cofree<Leaf, (Annotation, Annotation)>? {
let annotations = (a.extract, b.extract)
switch (a.unwrap, b.unwrap) {
case let (.Leaf, .Leaf(b)):
return Cofree(annotations, .Leaf(b))
case let (.Indexed(a), .Indexed(b)):
return Cofree(annotations, .Indexed(Swift.zip(a, b).flatMap(zip)))
case let (.Keyed(a), .Keyed(b)):
return Cofree(annotations, .Keyed(Dictionary(elements: b.keys.flatMap { key in zip(a[key]!, b[key]!).map { (key, $0) } })))
default:
return nil
}
}
} }
extension Cofree: CofreeType {} extension Cofree: CofreeType {}
@ -127,8 +152,8 @@ extension Cofree: CofreeType {}
extension CofreeType where Self.Annotation == Range<String.Index> { extension CofreeType where Self.Annotation == Range<String.Index> {
public func JSON(source: String) -> Doubt.JSON { public func JSON(source: String) -> Doubt.JSON {
return unwrap.JSON( return unwrap.JSON(
ifLeaf: { _ in .String(source[extract]) }, leaf: { _ in .String(source[extract]) },
ifRecur: { recur: {
[ [
"range": [ "range": [
"offset": .Number(Double(source.startIndex.distanceTo($0.extract.startIndex))), "offset": .Number(Double(source.startIndex.distanceTo($0.extract.startIndex))),

View File

@ -2,24 +2,26 @@
/// ///
/// This is free in the sense of unconstrained rather than zero-cost; its the monad obtained by taking a functor (in this case `Syntax`) and adding the minimum necessary details (the `Pure` case) to satisfy the monad laws. /// This is free in the sense of unconstrained rather than zero-cost; its the monad obtained by taking a functor (in this case `Syntax`) and adding the minimum necessary details (the `Pure` case) to satisfy the monad laws.
/// ///
/// `Syntax` is a non-recursive type parameterized by the type of its child nodes. Instantiating it to `Free` makes it recursive through the `Roll` case, and allows it to wrap values of type `B` through the `Pure` case. /// `Syntax` is a non-recursive type parameterized by the type of its child nodes. Instantiating it to `Free` makes it recursive through the `Roll` case, and allows it to wrap values of type `Value` through the `Pure` case.
/// ///
/// In Doubt, this allows us to represent diffs as values of the `Free` monad obtained from `Syntax`, injecting `Patch` into the tree; or otherwise put, a diff is a tree of mutually-recursive `Free.Roll`/`Syntax` nodes with `Pure` nodes injecting the actual changes. /// In Doubt, this allows us to represent diffs as values of the `Free` monad obtained from `Syntax`, injecting `Patch` into the tree; or otherwise put, a diff is a tree of mutually-recursive `Free.Roll`/`Syntax` nodes with `Pure` nodes injecting the actual changes.
public enum Free<A, B>: CustomDebugStringConvertible, SyntaxConvertible { public enum Free<Leaf, Annotation, Value>: CustomDebugStringConvertible {
/// The injection of a value of type `B` into the `Syntax` tree. /// The injection of a value of type `Value` into the `Syntax` tree.
case Pure(B) case Pure(Value)
/// A recursive instantiation of `Syntax`, unrolling another iteration of the recursive type. /// A recursive instantiation of `Syntax`, unrolling another iteration of the recursive type.
indirect case Roll(Syntax<Free, A>) indirect case Roll(Annotation, Syntax<Free, Leaf>)
/// Recursively copies a `Term: TermType where Term.LeafType == A` into a `Free<A, B>`, essentially mapping `Term.unwrap` onto `Free.Roll`. /// Construct a `Free` from a `CofreeType` with matching `Leaf` and `Annotation` types, copying the recursive structure of the term in via hylomorphism.
public init<Term: TermType where Term.LeafType == A>(_ term: Term) { ///
self = .Roll(term.unwrap.map(Free.init)) /// The resulting `Free` value will not have any `Pure` cases.
public init<Term: CofreeType where Term.Leaf == Leaf, Term.Annotation == Annotation>(_ term: Term) {
self = hylo(Free.Roll, Term.eliminate)(term)
} }
public func analysis<C>(@noescape ifPure ifPure: B -> C, @noescape ifRoll: Syntax<Free, A> -> C) -> C { public func analysis<C>(@noescape ifPure ifPure: Value -> C, @noescape ifRoll: (Annotation, Syntax<Free, Leaf>) -> C) -> C {
switch self { switch self {
case let .Pure(b): case let .Pure(b):
return ifPure(b) return ifPure(b)
@ -32,14 +34,14 @@ public enum Free<A, B>: CustomDebugStringConvertible, SyntaxConvertible {
/// ///
/// `Pure` values are simply unpacked. `Roll` values are mapped recursively, and then have `transform` applied to them. /// `Pure` values are simply unpacked. `Roll` values are mapped recursively, and then have `transform` applied to them.
/// ///
/// This forms a _catamorphism_ (from the Greek cata, downwards; compare catastrophe), a generalization of folds over regular trees (and datatypes isomorphic to them). It operates at the leaves first, and then branches near the periphery, recursively collapsing values by whatever is computed by `transform`. Catamorphisms are themselves an example of _recursion schemes_, which characterize specific well-behaved patterns of recursion. This gives `iterate` some useful properties for computations performed over trees. /// This forms a _catamorphism_ (from the Greek cata, downwards; compare catastrophe), a generalization of folds over regular trees (and datatypes isomorphic to them). It operates at the leaves first, and then branches near the periphery, recursively collapsing values by whatever is computed by `transform`. Catamorphisms are themselves an example of _recursion schemes_, which characterize specific well-behaved patterns of recursion. This gives `cata` some useful properties for computations performed over trees.
/// ///
/// Due to the character of recursion captured by catamorphisms, `iterate` ensures that computation will not only halt, but will further be linear in the size of the receiver. (Nesting a call to `iterate` will therefore result in O(n²) complexity.) This guarantee is achieved by careful composition of calls to `map` with recursive calls to `iterate`, only calling `transform` once the recursive call has completed. `transform` is itself non-recursive, receiving a `Syntax` whose recurrences have already been flattened to `B`. /// Due to the character of recursion captured by catamorphisms, `cata` ensures that computation will not only halt, but will further be linear in the size of the receiver. (Nesting a call to `cata` will therefore result in O(n²) complexity.) This guarantee is achieved by careful composition of calls to `map` with recursive calls to `cata`, only calling `transform` once the recursive call has completed. `transform` is itself non-recursive, receiving a `Syntax` whose recurrences have already been flattened to `Value`.
/// ///
/// The linearity of `iterate` in the size of the receiver makes it trivial to compute said size, by counting leaves as 1 and summing branches children: /// The linearity of `cata` in the size of the receiver makes it trivial to compute said size, by counting leaves as 1 and summing branches children:
/// ///
/// func size<A, B>(free: Free<A, B>) -> Int { /// func size<Leaf, Annotation, Value>(free: Free<Leaf, Annotation, Value>) -> Int {
/// return free.iterate { flattenedSyntax in /// return free.cata { flattenedSyntax in
/// switch flattenedSyntax { /// switch flattenedSyntax {
/// case .Leaf: /// case .Leaf:
/// return 1 /// return 1
@ -51,19 +53,19 @@ public enum Free<A, B>: CustomDebugStringConvertible, SyntaxConvertible {
/// } /// }
/// } /// }
/// ///
/// While not every function on a given `Free` can be computed using `iterate`, these guarantees of termination and complexity, as well as the brevity and focus on the operation being performed n times, make it a desirable scaffolding for any function which can. /// While not every function on a given `Free` can be computed using `cata`, these guarantees of termination and complexity, as well as the brevity and focus on the operation being performed n times, make it a desirable scaffolding for any function which can.
/// ///
/// For a lucid, in-depth tutorial on recursion schemes, I recommend [Patrick Thomson](https://twitter.com/importantshock)s _[An Introduction to Recursion Schemes](http://patrickthomson.ghost.io/an-introduction-to-recursion-schemes/)_ and _[Recursion Schemes, Part 2: A Mob of Morphisms](http://patrickthomson.ghost.io/recursion-schemes-part-2/)_. /// For a lucid, in-depth tutorial on recursion schemes, I recommend [Patrick Thomson](https://twitter.com/importantshock)s _[An Introduction to Recursion Schemes](http://patrickthomson.ghost.io/an-introduction-to-recursion-schemes/)_ and _[Recursion Schemes, Part 2: A Mob of Morphisms](http://patrickthomson.ghost.io/recursion-schemes-part-2/)_.
public func iterate(transform: Syntax<B, A> -> B) -> B { public func cata(transform: Syntax<Value, Leaf> -> Value) -> Value {
return analysis( return analysis(
ifPure: id, ifPure: id,
ifRoll: { transform($0.map { $0.iterate(transform) }) }) ifRoll: { $1.map { $0.cata(transform) } } >>> transform)
} }
/// Reduces the receiver top-down, left-to-right, starting from an `initial` value, and applying `combine` to successive values. /// Reduces the receiver top-down, left-to-right, starting from an `initial` value, and applying `combine` to successive values.
public func reduce(initial: B, combine: (B, B) -> B) -> B { public func reduce(initial: Value, combine: (Value, Value) -> Value) -> Value {
return iterate { return cata {
switch $0 { switch $0 {
case .Leaf: case .Leaf:
return initial return initial
@ -76,22 +78,22 @@ public enum Free<A, B>: CustomDebugStringConvertible, SyntaxConvertible {
} }
/// Returns a function which sums `Free`s by first `transform`ing `Pure` values into integers, and then summing these. /// Returns a function which sums `Free`s by first `transform`ing `Pure` values into integers, and then summing these.
public static func sum(transform: B -> Int)(_ free: Free) -> Int { public static func sum(transform: Value -> Int)(_ free: Free) -> Int {
return free.map(transform).reduce(0, combine: +) return free.map(transform).reduce(0, combine: +)
} }
// MARK: Functor // MARK: Functor
public func map<C>(@noescape transform: B -> C) -> Free<A, C> { public func map<C>(@noescape transform: Value -> C) -> Free<Leaf, Annotation, C> {
return analysis(ifPure: { .Pure(transform($0)) }, ifRoll: { .Roll($0.map { $0.map(transform) }) }) return analysis(ifPure: { .Pure(transform($0)) }, ifRoll: { .Roll($0, $1.map { $0.map(transform) }) })
} }
// MARK: Monad // MARK: Monad
public func flatMap<C>(@noescape transform: B -> Free<A, C>) -> Free<A, C> { public func flatMap<C>(@noescape transform: Value -> Free<Leaf, Annotation, C>) -> Free<Leaf, Annotation, C> {
return analysis(ifPure: transform, ifRoll: { .Roll($0.map { $0.flatMap(transform) }) }) return analysis(ifPure: transform, ifRoll: { .Roll($0, $1.map { $0.flatMap(transform) }) })
} }
@ -105,40 +107,37 @@ public enum Free<A, B>: CustomDebugStringConvertible, SyntaxConvertible {
return ".Roll(\(String(reflecting: s)))" return ".Roll(\(String(reflecting: s)))"
} }
} }
// MARK: SyntaxConvertible
public init(syntax: Syntax<Free, A>) {
self = .Roll(syntax)
}
} }
// MARK: - Anamorphism // MARK: - Anamorphism
extension Free { extension Free {
public static func Introduce(annotation: Annotation)(syntax: Syntax<Free, Leaf>) -> Free {
return Roll(annotation, syntax)
}
/// Anamorphism over `Free`. /// Anamorphism over `Free`.
/// ///
/// Unfolds a tree bottom-up by recursively applying `transform` to a series of values starting with `seed`. Since `Syntax.Leaf` does not recur, this will halt when it has produced leaves for every branch. /// Unfolds a tree bottom-up by recursively applying `transform` to a series of values starting with `seed`. Since `Syntax.Leaf` does not recur, this will halt when it has produced leaves for every branch.
public static func ana<Seed>(transform: Seed -> Syntax<Seed, A>)(_ seed: Seed) -> Free { public static func ana(unfold: Annotation -> Syntax<Annotation, Leaf>)(_ seed: Annotation) -> Free {
return (Roll <<< { $0.map(ana(transform)) } <<< transform)(seed) return (Introduce(seed) <<< { $0.map(ana(unfold)) } <<< unfold) <| seed
} }
} }
extension Free where B: PatchType, B.Element == Cofree<A, ()> { extension Free where Value: PatchType, Value.Element == Cofree<Leaf, ()> {
public typealias Term = B.Element public typealias Term = Value.Element
public func merge(transform: B -> Term) -> Term { public func merge(transform: Value -> Term) -> Term {
return map(transform).iterate { Cofree((), $0) } return map(transform).cata { Cofree((), $0) }
} }
public func merge(transform: B -> Term?) -> Term? { public func merge(transform: Value -> Term?) -> Term? {
return map(transform).iterate(Free.discardNullTerms) return map(transform).cata(Free.discardNullTerms)
} }
private static func discardNullTerms(syntax: Syntax<Term?, A>) -> Term? { private static func discardNullTerms(syntax: Syntax<Term?, Leaf>) -> Term? {
switch syntax { switch syntax {
case let .Leaf(a): case let .Leaf(a):
return Cofree((), .Leaf(a)) return Cofree((), .Leaf(a))
@ -161,17 +160,17 @@ extension Free where B: PatchType, B.Element == Cofree<A, ()> {
// MARK: - Patch construction // MARK: - Patch construction
extension Free where B: PatchType { extension Free where Value: PatchType {
public static func Replace(before: B.Element, _ after: B.Element) -> Free { public static func Replace(before: Value.Element, _ after: Value.Element) -> Free {
return .Pure(B(replacing: before, with: after)) return .Pure(Value(replacing: before, with: after))
} }
public static func Insert(after: B.Element) -> Free { public static func Insert(after: Value.Element) -> Free {
return .Pure(B(inserting: after)) return .Pure(Value(inserting: after))
} }
public static func Delete(before: B.Element) -> Free { public static func Delete(before: Value.Element) -> Free {
return .Pure(B(deleting: before)) return .Pure(Value(deleting: before))
} }
@ -184,46 +183,45 @@ extension Free where B: PatchType {
// MARK: - Equality // MARK: - Equality
extension Free { extension Free {
public static func equals(ifPure ifPure: (B, B) -> Bool, ifRoll: (A, A) -> Bool)(_ left: Free, _ right: Free) -> Bool { public static func equals(pure pure: (Value, Value) -> Bool, leaf: (Leaf, Leaf) -> Bool, annotation: (Annotation, Annotation) -> Bool)(_ left: Free, _ right: Free) -> Bool {
switch (left, right) { switch (left, right) {
case let (.Pure(a), .Pure(b)): case let (.Pure(a), .Pure(b)):
return ifPure(a, b) return pure(a, b)
case let (.Roll(a), .Roll(b)): case let (.Roll(annotation1, syntax1), .Roll(annotation2, syntax2)):
return Syntax.equals(ifLeaf: ifRoll, ifRecur: equals(ifPure: ifPure, ifRoll: ifRoll))(a, b) return annotation(annotation1, annotation2) && Syntax.equals(leaf: leaf, recur: equals(pure: pure, leaf: leaf, annotation: annotation))(syntax1, syntax2)
default: default:
return false return false
} }
} }
} }
public func == <A: Equatable, B: Equatable> (left: Free<A, B>, right: Free<A, B>) -> Bool { public func == <Leaf: Equatable, Value: Equatable, Annotation: Equatable> (left: Free<Leaf, Annotation, Value>, right: Free<Leaf, Annotation, Value>) -> Bool {
return Free.equals(ifPure: ==, ifRoll: ==)(left, right) return Free.equals(pure: ==, leaf: ==, annotation: ==)(left, right)
} }
public func == <Term: TermType where Term.LeafType: Equatable> (left: Free<Term.LeafType, Patch<Term>>, right: Free<Term.LeafType, Patch<Term>>) -> Bool { public func == <Term: CofreeType, Annotation: Equatable where Term.Leaf: Equatable> (left: Free<Term.Leaf, Annotation, Patch<Term>>, right: Free<Term.Leaf, Annotation, Patch<Term>>) -> Bool {
return Free.equals(ifPure: Patch.equals(Term.equals(==)), ifRoll: ==)(left, right) return Free.equals(pure: Patch.equals(Term.equals(==)), leaf: ==, annotation: ==)(left, right)
}
public func == <Term: CofreeType, Annotation where Term.Leaf: Equatable> (left: Free<Term.Leaf, Annotation, Patch<Term>>, right: Free<Term.Leaf, Annotation, Patch<Term>>) -> Bool {
return Free.equals(pure: Patch.equals(Term.equals(==)), leaf: ==, annotation: const(true))(left, right)
} }
// MARK: - JSON // MARK: - JSON
extension Free { extension Free {
public func JSON(ifPure ifPure: B -> Doubt.JSON, ifLeaf: A -> Doubt.JSON) -> Doubt.JSON { public func JSON(pure pure: Value -> Doubt.JSON, leaf: Leaf -> Doubt.JSON, annotation: Annotation -> Doubt.JSON) -> Doubt.JSON {
return analysis( return analysis(
ifPure: { ifPure: { [ "pure": pure($0) ] },
[ "pure": ifPure($0) ]
},
ifRoll: { ifRoll: {
[ "roll": $0.JSON(ifLeaf: ifLeaf, ifRecur: { $0.JSON(ifPure: ifPure, ifLeaf: ifLeaf) }) ] [ "roll": [
"extract": annotation($0),
"unwrap": $1.JSON(leaf: leaf, recur: { $0.JSON(pure: pure, leaf: leaf, annotation: annotation) })
] ]
}) })
} }
} }
extension Free where A: CustomJSONConvertible {
public func JSON(ifPure: B -> Doubt.JSON) -> Doubt.JSON {
return JSON(ifPure: ifPure, ifLeaf: { $0.JSON })
}
}
import Prelude import Prelude

View File

@ -1,7 +1,7 @@
/// An interpreter of `Algorithm`s. /// An interpreter of `Algorithm`s.
public struct Interpreter<Term: TermType> { public struct Interpreter<Term: CofreeType> {
/// The type of diffs constructed by `Interpreter`s. /// The type of diffs constructed by `Interpreter`s.
public typealias Diff = Free<Term.LeafType, Patch<Term>> public typealias Diff = Free<Term.Leaf, (Term.Annotation, Term.Annotation), Patch<Term>>
/// Constructs an `Interpreter` parameterized by the `equal` and `comparable` tests on `Term`s, and the `cost` function for `Diff`s. /// Constructs an `Interpreter` parameterized by the `equal` and `comparable` tests on `Term`s, and the `cost` function for `Diff`s.
/// ///
@ -42,15 +42,21 @@ public struct Interpreter<Term: TermType> {
/// Diff `a` against `b`, if comparable. /// Diff `a` against `b`, if comparable.
private func recur(a: Term, _ b: Term) -> Diff? { private func recur(a: Term, _ b: Term) -> Diff? {
if equal(a, b) { return Diff.ana(Term.unwrap)(b) } // If both terms are equal, we dont need to bother diffing.
//
// In that case, zip the two terms together (to pair their annotations), and then map the resulting `Term` (which, since the terms are equal, will be non-nil) into a `Diff`.
if equal(a, b) { return Term.zip(a, b).map(Diff.init) }
guard comparable(a, b) else { return nil } guard comparable(a, b) else { return nil }
let algorithm: Algorithm<Term, Diff> let algorithm: Algorithm<Term, Diff>
let annotations = (a.extract, b.extract)
switch (a.unwrap, b.unwrap) { switch (a.unwrap, b.unwrap) {
case let (.Leaf, .Leaf(leaf)) where equal(a, b):
return .Roll(annotations, .Leaf(leaf))
case let (.Keyed(a), .Keyed(b)): case let (.Keyed(a), .Keyed(b)):
algorithm = .Roll(.ByKey(a, b, Syntax.Keyed >>> Diff.Roll >>> Algorithm.Pure)) algorithm = .Roll(.ByKey(a, b, Syntax.Keyed >>> Diff.Introduce(annotations) >>> Algorithm.Pure))
case let (.Indexed(a), .Indexed(b)): case let (.Indexed(a), .Indexed(b)):
algorithm = .Roll(.ByIndex(a, b, Syntax.Indexed >>> Diff.Roll >>> Algorithm.Pure)) algorithm = .Roll(.ByIndex(a, b, Syntax.Indexed >>> Diff.Introduce(annotations) >>> Algorithm.Pure))
default: default:
algorithm = .Roll(.Recursive(a, b, Algorithm.Pure)) algorithm = .Roll(.Recursive(a, b, Algorithm.Pure))
} }
@ -64,12 +70,13 @@ public struct Interpreter<Term: TermType> {
case let .Roll(.Recursive(a, b, f)): case let .Roll(.Recursive(a, b, f)):
// Recur structurally into both terms, patching differing sub-terms. This is akin to unification, except that it computes a patched tree instead of a substitution. Its also a little like a structural zip on pairs of terms. // Recur structurally into both terms, patching differing sub-terms. This is akin to unification, except that it computes a patched tree instead of a substitution. Its also a little like a structural zip on pairs of terms.
let annotations = (a.extract, b.extract)
switch (a.unwrap, b.unwrap) { switch (a.unwrap, b.unwrap) {
case let (.Indexed(a), .Indexed(b)) where a.count == b.count: case let (.Indexed(a), .Indexed(b)) where a.count == b.count:
return recur(f(.Indexed(zip(a, b).map(run)))) return recur(f(.Roll(annotations, .Indexed(zip(a, b).map(run)))))
case let (.Keyed(a), .Keyed(b)) where Array(a.keys) == Array(b.keys): case let (.Keyed(a), .Keyed(b)) where Array(a.keys) == Array(b.keys):
return recur(f(.Keyed(Dictionary(elements: b.keys.map { ($0, self.run(a[$0]!, b[$0]!)) })))) return recur(f(.Roll(annotations, .Keyed(Dictionary(elements: b.keys.map { ($0, self.run(a[$0]!, b[$0]!)) })))))
default: default:
// This must not call `recur` directly with `a` and `b`, as that would infinite loop if actually recursive. // This must not call `recur` directly with `a` and `b`, as that would infinite loop if actually recursive.
@ -92,7 +99,7 @@ public struct Interpreter<Term: TermType> {
// MARK: - Constrained constructors // MARK: - Constrained constructors
extension Interpreter where Term.LeafType: Equatable { extension Interpreter where Term.Leaf: Equatable {
public init(comparable: (Term, Term) -> Bool, cost: Diff -> Int) { public init(comparable: (Term, Term) -> Bool, cost: Diff -> Int) {
self.init(equal: Term.equals(==), comparable: comparable, cost: cost) self.init(equal: Term.equals(==), comparable: comparable, cost: cost)
} }

View File

@ -10,7 +10,7 @@ import Foundation
import Doubt import Doubt
typealias Term = Cofree<JSONLeaf, Range<String.Index>> typealias Term = Cofree<JSONLeaf, Range<String.Index>>
typealias Diff = Free<JSONLeaf, Patch<Term>> typealias Diff = Free<JSONLeaf, Term.Annotation, Patch<Term>>
enum JSONLeaf: CustomJSONConvertible, CustomStringConvertible, Equatable { enum JSONLeaf: CustomJSONConvertible, CustomStringConvertible, Equatable {
case Number(Double) case Number(Double)

View File

@ -130,7 +130,7 @@ extension Patch where A: CustomJSONConvertible {
// MARK: - PatchType // MARK: - PatchType
/// A hack to enable constrained extensions on `Free<A, Patch<Term: TermType where LeafType == A>`. /// A hack to enable constrained extensions on `Free<Leaf, Annotation, Patch<Term: CofreeType where Term.Leaf == Leaf, Term.Annotation == Annotation>`.
public protocol PatchType { public protocol PatchType {
typealias Element typealias Element

View File

@ -1,8 +1,8 @@
/// 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`. /// Computes the SES (shortest edit script), i.e. the shortest sequence of diffs (`Free<Leaf, Annotation, 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. /// 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<Term, A>(a: [Term], _ b: [Term], cost: Free<A, Patch<Term>> -> Int, recur: (Term, Term) -> Free<A, Patch<Term>>?) -> [Free<A, Patch<Term>>] { public func SES<Term, Leaf, Annotation>(a: [Term], _ b: [Term], cost: Free<Leaf, Annotation, Patch<Term>> -> Int, recur: (Term, Term) -> Free<Leaf, Annotation, Patch<Term>>?) -> [Free<Leaf, Annotation, Patch<Term>>] {
typealias Diff = Free<A, Patch<Term>> typealias Diff = Free<Leaf, Annotation, Patch<Term>>
if a.isEmpty { return b.map { .Insert($0) } } if a.isEmpty { return b.map { .Insert($0) } }
if b.isEmpty { return a.map { .Delete($0) } } if b.isEmpty { return a.map { .Delete($0) } }

View File

@ -18,6 +18,9 @@ public enum Syntax<Recur, A>: CustomDebugStringConvertible {
} }
} }
// MARK: CustomDebugStringConvertible
public var debugDescription: String { public var debugDescription: String {
switch self { switch self {
case let .Leaf(n): case let .Leaf(n):
@ -31,6 +34,31 @@ public enum Syntax<Recur, A>: CustomDebugStringConvertible {
} }
// MARK: - Hylomorphism
/// Hylomorphism through `Syntax`.
///
/// A hylomorphism (from the Aristotelian philosophy that form and matter are one) is a function of type `A B` whose call-tree is linear in the size of the nodes produced by `up`. Conceptually, its the composition of a catamorphism (see also `cata`) and an anamorphism (see also `ana`), but is implemented by [Stream fusion](http://lambda-the-ultimate.org/node/2192) and as such enjoys O(n) time complexity, O(1) size complexity, and small constant factors for both (modulo inadvisable implementations of `up` and `down`).
///
/// Hylomorphisms are used to construct diffs corresponding to equal terms; see also `CofreeType.zip`.
///
/// `hylo` can be used with arbitrary functors which can eliminate to and introduce with `Syntax` values.
public func hylo<A, B, Leaf>(down: Syntax<B, Leaf> -> B, _ up: A -> Syntax<A, Leaf>) -> A -> B {
return up >>> { $0.map(hylo(down, up)) } >>> down
}
/// Reiteration through `Syntax`.
///
/// This is a form of hylomorphism (from the Aristotelian philosophy that form and matter are one). As such, it returns a function of type `A B` whose call-tree is linear in the size of the nodes produced by `up`. Conceptually, its the composition of a catamorphism (see also `cata`) and an anamorphism (see also `ana`), but is implemented by [Stream fusion](http://lambda-the-ultimate.org/node/2192) and as such enjoys O(n) time complexity, O(1) size complexity, and small constant factors for both (modulo inadvisable implementations of `up` and `down`).
///
/// Hylomorphisms are used to construct diffs corresponding to equal terms; see also `CofreeType.zip`.
///
/// `hylo` can be used with arbitrary functors which can eliminate to and introduce with `Annotation` & `Syntax` pairs.
public func hylo<A, B, Leaf, Annotation>(down: (Annotation, Syntax<B, Leaf>) -> B, _ up: A -> (Annotation, Syntax<A, Leaf>)) -> A -> B {
return up >>> { ($0, $1.map(hylo(down, up))) } >>> down
}
// MARK: - ArrayLiteralConvertible // MARK: - ArrayLiteralConvertible
extension Syntax: ArrayLiteralConvertible { extension Syntax: ArrayLiteralConvertible {
@ -52,14 +80,14 @@ extension Syntax: DictionaryLiteralConvertible {
// MARK: - Equality // MARK: - Equality
extension Syntax { extension Syntax {
public static func equals(ifLeaf ifLeaf: (A, A) -> Bool, ifRecur: (Recur, Recur) -> Bool)(_ left: Syntax<Recur, A>, _ right: Syntax<Recur, A>) -> Bool { public static func equals(leaf leaf: (A, A) -> Bool, recur: (Recur, Recur) -> Bool)(_ left: Syntax<Recur, A>, _ right: Syntax<Recur, A>) -> Bool {
switch (left, right) { switch (left, right) {
case let (.Leaf(l1), .Leaf(l2)): case let (.Leaf(l1), .Leaf(l2)):
return ifLeaf(l1, l2) return leaf(l1, l2)
case let (.Indexed(v1), .Indexed(v2)): case let (.Indexed(v1), .Indexed(v2)):
return v1.count == v2.count && zip(v1, v2).lazy.map(ifRecur).reduce(true) { $0 && $1 } return v1.count == v2.count && zip(v1, v2).lazy.map(recur).reduce(true) { $0 && $1 }
case let (.Keyed(d1), .Keyed(d2)): case let (.Keyed(d1), .Keyed(d2)):
return Set(d1.keys) == Set(d2.keys) && d1.keys.map { ifRecur(d1[$0]!, d2[$0]!) }.reduce(true) { $0 && $1 } return Set(d1.keys) == Set(d2.keys) && d1.keys.map { recur(d1[$0]!, d2[$0]!) }.reduce(true) { $0 && $1 }
default: default:
return false return false
} }
@ -67,46 +95,24 @@ extension Syntax {
} }
public func == <F: Equatable, A: Equatable> (left: Syntax<F, A>, right: Syntax<F, A>) -> Bool { public func == <F: Equatable, A: Equatable> (left: Syntax<F, A>, right: Syntax<F, A>) -> Bool {
return Syntax.equals(ifLeaf: ==, ifRecur: ==)(left, right) return Syntax.equals(leaf: ==, recur: ==)(left, right)
} }
// MARK: - JSON // MARK: - JSON
extension Syntax { extension Syntax {
public func JSON(@noescape ifLeaf ifLeaf: A -> Doubt.JSON, @noescape ifRecur: Recur -> Doubt.JSON) -> Doubt.JSON { public func JSON(@noescape leaf leaf: A -> Doubt.JSON, @noescape recur: Recur -> Doubt.JSON) -> Doubt.JSON {
switch self { switch self {
case let .Leaf(a): case let .Leaf(a):
return ifLeaf(a) return leaf(a)
case let .Indexed(a): case let .Indexed(a):
return .Array(a.map(ifRecur)) return .Array(a.map(recur))
case let .Keyed(d): case let .Keyed(d):
return .Dictionary(Dictionary(elements: d.map { ($0, ifRecur($1)) })) return .Dictionary(Dictionary(elements: d.map { ($0, recur($1)) }))
} }
} }
} }
// MARK: - Construction import Prelude
/// SyntaxConvertible types can be constructed with the same constructors available on Syntax itself, as a convenience.
public protocol SyntaxConvertible {
typealias RecurType
typealias LeafType
init(syntax: Syntax<RecurType, LeafType>)
}
extension SyntaxConvertible {
public static func Leaf(value: LeafType) -> Self {
return Self(syntax: .Leaf(value))
}
public static func Indexed(children: [RecurType]) -> Self {
return Self(syntax: .Indexed(children))
}
public static func Keyed(children: [String:RecurType]) -> Self {
return Self(syntax: .Keyed(children))
}
}

View File

@ -1,27 +1,27 @@
/// The type of terms. /// The type of terms.
public protocol TermType { public protocol TermType {
typealias LeafType typealias Leaf
var unwrap: Syntax<Self, LeafType> { get } var unwrap: Syntax<Self, Leaf> { get }
} }
extension TermType { extension TermType {
public static func unwrap(term: Self) -> Syntax<Self, LeafType> { public static func unwrap(term: Self) -> Syntax<Self, Leaf> {
return term.unwrap return term.unwrap
} }
/// Catamorphism over `TermType`s. /// Catamorphism over `TermType`s.
/// ///
/// Folds the tree encoded by the receiver into a single value by recurring top-down through the tree, applying `transform` to leaves, then to branches, and so forth. /// Folds the tree encoded by the receiver into a single value by recurring top-down through the tree, applying `transform` to leaves, then to branches, and so forth.
public func cata<Result>(transform: Syntax<Result, LeafType> -> Result) -> Result { public func cata<Result>(transform: Syntax<Result, Leaf> -> Result) -> Result {
return self |> (Self.unwrap >>> { $0.map { $0.cata(transform) } } >>> transform) return self |> (Self.unwrap >>> { $0.map { $0.cata(transform) } } >>> transform)
} }
/// Paramorphism over `TermType`s. /// Paramorphism over `TermType`s.
/// ///
/// Folds the tree encoded by the receiver into a single value by recurring top-down through the tree, applying `transform` to leaves, then to branches, and so forth. Each recursive instance is made available in the `Syntax` alongside the result value at that node. /// Folds the tree encoded by the receiver into a single value by recurring top-down through the tree, applying `transform` to leaves, then to branches, and so forth. Each recursive instance is made available in the `Syntax` alongside the result value at that node.
public func para<Result>(transform: Syntax<(Self, Result), LeafType> -> Result) -> Result { public func para<Result>(transform: Syntax<(Self, Result), Leaf> -> Result) -> Result {
return self |> (Self.unwrap >>> { $0.map { ($0, $0.para(transform)) } } >>> transform) return self |> (Self.unwrap >>> { $0.map { ($0, $0.para(transform)) } } >>> transform)
} }
@ -44,14 +44,11 @@ extension TermType {
} }
extension Cofree: TermType {}
// MARK: - Equality // MARK: - Equality
extension TermType { extension TermType {
public static func equals(leaf: (LeafType, LeafType) -> Bool)(_ a: Self, _ b: Self) -> Bool { public static func equals(leaf: (Leaf, Leaf) -> Bool)(_ a: Self, _ b: Self) -> Bool {
return Syntax.equals(ifLeaf: leaf, ifRecur: equals(leaf))(a.unwrap, b.unwrap) return Syntax.equals(leaf: leaf, recur: equals(leaf))(a.unwrap, b.unwrap)
} }
} }

View File

@ -4,7 +4,7 @@ final class DiffTests: XCTestCase {
} }
typealias Term = RangedTerm.Term typealias Term = RangedTerm.Term
typealias Diff = Free<String, Patch<Term>> typealias Diff = Free<String, (Term.Annotation, Term.Annotation), Patch<Term>>
let interpreter = Interpreter<Term>(equal: ==, comparable: const(true), cost: Diff.sum(const(1))) let interpreter = Interpreter<Term>(equal: ==, comparable: const(true), cost: Diff.sum(const(1)))
@ -14,6 +14,12 @@ final class DiffTests: XCTestCase {
} }
} }
func testRecursivelyCopiedDiffsHaveNoPatches() {
property("recursively copying a term into a diff produces no patches") <- forAll { (term: RangedTerm) in
Free.sum(const(1))(Free<Term.Leaf, Term.Annotation, Patch<Term>>(term.term)) == 0
}
}
func testInequalTermsProduceNonIdentityDiffs() { func testInequalTermsProduceNonIdentityDiffs() {
property("inequal terms produce non-identity diffs") <- forAll { (diff: RangedDiff) in property("inequal terms produce non-identity diffs") <- forAll { (diff: RangedDiff) in
(!Term.equals(annotation: const(true), leaf: ==)(diff.a.term, diff.b.term)) ==> Diff.sum(const(1))(diff.diff) > 0 (!Term.equals(annotation: const(true), leaf: ==)(diff.a.term, diff.b.term)) ==> Diff.sum(const(1))(diff.diff) > 0
@ -50,7 +56,7 @@ final class DiffTests: XCTestCase {
private func equal(a: DiffTests.Diff, _ b: DiffTests.Diff) -> Bool { private func equal(a: DiffTests.Diff, _ b: DiffTests.Diff) -> Bool {
return Free.equals(ifPure: Patch.equals(Cofree.equals(annotation: ==, leaf: ==)), ifRoll: ==)(a, b) return Free.equals(pure: Patch.equals(Cofree.equals(annotation: ==, leaf: ==)), leaf: ==, annotation: const(true))(a, b)
} }

View File

@ -1,7 +1,9 @@
final class InterpreterTests: XCTestCase { final class InterpreterTests: XCTestCase {
func testRestrictsComparisons() { func testRestrictsComparisons() {
let comparable: (Term, Term) -> Bool = { $0.extract == 0 && $1.extract == 0 } let comparable: (Term, Term) -> Bool = { $0.extract == 0 && $1.extract == 0 }
assert(Interpreter(equal: ==, comparable: comparable, cost: const(1)).run(a, b), ==, restricted) let i = Interpreter(equal: ==, comparable: comparable, cost: const(1))
let d = i.run(a, b)
assert(d, ==, restricted)
} }
func testComparisonsOfDisjointlyCategorizedTermsAreRestricted() { func testComparisonsOfDisjointlyCategorizedTermsAreRestricted() {
@ -21,22 +23,22 @@ final class InterpreterTests: XCTestCase {
private typealias Term = Cofree<String, Int> private typealias Term = Cofree<String, Int>
private typealias Diff = Free<String, Patch<Term>> private typealias Diff = Free<String, (Int, Int), Patch<Term>>
private let a = Term(0, [ Term(1, .Leaf("a")), Term(2, .Leaf("b")), Term(3, .Leaf("c")) ]) private let a = Term(0, [ Term(1, .Leaf("a")), Term(2, .Leaf("b")), Term(3, .Leaf("c")) ])
private let b = Term(0, [ Term(1, .Leaf("c")), Term(2, .Leaf("b")), Term(3, .Leaf("a")) ]) private let b = Term(0, [ Term(1, .Leaf("c")), Term(2, .Leaf("b")), Term(3, .Leaf("a")) ])
private let restricted = Diff.Roll([ private let restricted = Diff.Roll((0, 0), [
.Pure(.Insert(Term(1, .Leaf("c")))), .Pure(.Insert(Term(1, .Leaf("c")))),
.Pure(.Delete(Term(1, .Leaf("a")))), .Pure(.Delete(Term(1, .Leaf("a")))),
Diff(Term(2, .Leaf("b"))), .Roll((2, 2), .Leaf("b")),
.Pure(.Insert(Term(3, .Leaf("a")))), .Pure(.Insert(Term(3, .Leaf("a")))),
.Pure(.Delete(Term(3, .Leaf("c")))), .Pure(.Delete(Term(3, .Leaf("c")))),
]) ])
private let unrestricted = Diff.Roll([ private let unrestricted = Diff.Roll((0, 0), [
.Pure(.Replace(Term(1, .Leaf("a")), Term(1, .Leaf("c")))), .Pure(.Replace(Term(1, .Leaf("a")), Term(1, .Leaf("c")))),
Diff(Term(2, .Leaf("b"))), .Roll((2, 2), .Leaf("b")),
.Pure(.Replace(Term(3, .Leaf("c")), Term(3, .Leaf("a")))), .Pure(.Replace(Term(3, .Leaf("c")), Term(3, .Leaf("a")))),
]) ])

View File

@ -1,5 +1,5 @@
struct RangedDiff { struct RangedDiff {
typealias Diff = Free<String, Patch<RangedTerm.Term>> typealias Diff = Free<String, (RangedTerm.Term.Annotation, RangedTerm.Term.Annotation), Patch<RangedTerm.Term>>
let a: RangedTerm let a: RangedTerm
let b: RangedTerm let b: RangedTerm

View File

@ -12,36 +12,40 @@ final class SESTests: XCTestCase {
} }
func testSESCanInsertAtHead() { func testSESCanInsertAtHead() {
assert(SES([ a, b, c ], [ d, a, b, c ]), ==, [ .Insert(d), Diff(a), Diff(b), Diff(c) ]) assert(SES([ a, b, c ], [ d, a, b, c ]), ==, [ .Insert(d), Copy(a), Copy(b), Copy(c) ])
} }
func testSESCanDeleteAtHead() { func testSESCanDeleteAtHead() {
assert(SES([ d, a, b, c ], [ a, b, c ]), ==, [ .Delete(d), Diff(a), Diff(b), Diff(c) ]) assert(SES([ d, a, b, c ], [ a, b, c ]), ==, [ .Delete(d), Copy(a), Copy(b), Copy(c) ])
} }
func testSESCanInsertInMiddle() { func testSESCanInsertInMiddle() {
assert(SES([ a, b, c ], [ a, d, b, c ]), ==, [ Diff(a), .Insert(d), Diff(b), Diff(c) ]) assert(SES([ a, b, c ], [ a, d, b, c ]), ==, [ Copy(a), .Insert(d), Copy(b), Copy(c) ])
} }
func testSESCanDeleteInMiddle() { func testSESCanDeleteInMiddle() {
assert(SES([ a, d, b, c ], [ a, b, c ]), ==, [ Diff(a), .Delete(d), Diff(b), Diff(c) ]) assert(SES([ a, d, b, c ], [ a, b, c ]), ==, [ Copy(a), .Delete(d), Copy(b), Copy(c) ])
} }
func testInsertsAtEnd() { func testInsertsAtEnd() {
assert(SES([ a, b, c ], [ a, b, c, d ]), ==, [ Diff(a), Diff(b), Diff(c), .Insert(d) ]) assert(SES([ a, b, c ], [ a, b, c, d ]), ==, [ Copy(a), Copy(b), Copy(c), .Insert(d) ])
} }
func testDeletesAtEnd() { func testDeletesAtEnd() {
assert(SES([ a, b, c, d ], [ a, b, c ]), ==, [ Diff(a), Diff(b), Diff(c), .Delete(d) ]) assert(SES([ a, b, c, d ], [ a, b, c ]), ==, [ Copy(a), Copy(b), Copy(c), .Delete(d) ])
} }
func testSESOfLongerSequences() { func testSESOfLongerSequences() {
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) ]) assert(SES([ a, b, c, a, b, b, a ], [ c, b, a, b, a, c ]), ==, [ .Insert(c), .Delete(a), Copy(b), .Delete(c), Copy(a), .Delete(b), Copy(b), Copy(a), .Insert(c) ])
} }
} }
private typealias Term = Cofree<String, ()> private typealias Term = Cofree<String, ()>
private typealias Diff = Free<String, Patch<Term>> private typealias Diff = Free<String, (), Patch<Term>>
private func Copy(term: Term) -> Diff {
return hylo(Diff.Introduce(()), Term.unwrap)(term)
}
private let a = Term((), .Leaf("a")) private let a = Term((), .Leaf("a"))
private let b = Term((), .Leaf("b")) private let b = Term((), .Leaf("b"))
@ -49,11 +53,11 @@ private let c = Term((), .Leaf("c"))
private let d = Term((), .Leaf("d")) private let d = Term((), .Leaf("d"))
private func SES(a: [Term], _ b: [Term]) -> [Diff] { private func SES(a: [Term], _ b: [Term]) -> [Diff] {
return SES(a, b, cost: const(1)) { Cofree.equals(annotation: const(true), leaf: ==)($0, $1) ? Diff($1) : nil } return SES(a, b, cost: const(1)) { Cofree.equals(annotation: const(true), leaf: ==)($0, $1) ? Copy($1) : nil }
} }
private func == (a: [Diff], b: [Diff]) -> Bool { private func == (a: [Diff], b: [Diff]) -> Bool {
return a.count == b.count && zip(a, b).lazy.map(Diff.equals(ifPure: Patch.equals(Cofree.equals(annotation: const(true), leaf: ==)), ifRoll: ==)).reduce(true) { $0 && $1 } return a.count == b.count && zip(a, b).lazy.map(Diff.equals(pure: Patch.equals(Cofree.equals(annotation: const(true), leaf: ==)), leaf: ==, annotation: const(true))).reduce(true) { $0 && $1 }
} }

View File

@ -8,6 +8,12 @@ final class TermTests: XCTestCase {
Cofree.equals(annotation: ==, leaf: ==)(term.term, term.term) Cofree.equals(annotation: ==, leaf: ==)(term.term, term.term)
} }
} }
func testEqualTermsZipCleanly() {
property("equal terms zip to a non-nil value") <- forAll { (term: RangedTerm) in
Cofree.zip(term.term, term.term) != nil
}
}
} }

View File

@ -55,7 +55,12 @@ func diffAndSerialize(a aString: String, b bString: String, to: String) throws {
[ [
"a": .String(aString), "a": .String(aString),
"b": .String(bString), "b": .String(bString),
"diff": diff.JSON(ifPure: { $0.JSON { $0.JSON(annotation: range, leaf: { $0.JSON }) } }, ifLeaf: { $0.JSON }), "diff": diff.JSON(pure: { $0.JSON { $0.JSON(annotation: range, leaf: { $0.JSON }) } }, leaf: { $0.JSON }, annotation: {
[
"a": range($0),
"b": range($1),
]
}),
] ]
} }
@ -64,10 +69,6 @@ func diffAndSerialize(a aString: String, b bString: String, to: String) throws {
} }
try data.writeToFile(to, options: .DataWritingAtomic) try data.writeToFile(to, options: .DataWritingAtomic)
return benchmark("decoding data into string") {
NSString(data: data, encoding: NSUTF8StringEncoding) as String?
}
} }
let readFile = { (path: String) -> String? in let readFile = { (path: String) -> String? in