1
1
mirror of https://github.com/github/semantic.git synced 2024-11-25 11:04:00 +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.
///
/// 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.
public typealias Patch = Doubt.Patch<Term>
/// 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`.
///

View File

@ -1,12 +1,12 @@
/// 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.
public enum Cofree<A, B> {
indirect case Unroll(B, Syntax<Cofree, A>)
/// 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<Leaf, Annotation> {
indirect case Unroll(Annotation, Syntax<Cofree, Leaf>)
public var unwrap: Syntax<Cofree, A> {
public var unwrap: Syntax<Cofree, Leaf> {
switch self {
case let .Unroll(_, 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)
}
/// 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
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) })
}
}
@ -52,7 +42,7 @@ extension Cofree {
extension Cofree {
/// Returns the value annotating the syntax tree at this node.
public var extract: B {
public var extract: Annotation {
switch self {
case let .Unroll(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.
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) })
}
/// 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)
}
}
@ -74,13 +64,13 @@ extension Cofree {
// MARK: - Equality
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)
&& 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)
}
@ -88,15 +78,15 @@ public func == <A: Equatable, B: Equatable> (left: Cofree<A, B>, right: Cofree<A
// MARK: - JSON
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 [
"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 {
return JSON(annotation: { $0.JSON }, leaf: { $0.JSON })
}
@ -105,8 +95,8 @@ extension Cofree where A: CustomJSONConvertible, B: CustomJSONConvertible {
// MARK: - Categorizable
extension Cofree where B: Categorizable {
var categories: Set<B.Category> {
extension Cofree where Annotation: Categorizable {
var categories: Set<Annotation.Category> {
return extract.categories
}
}
@ -114,12 +104,47 @@ extension Cofree where B: Categorizable {
// MARK: - CofreeType
public protocol CofreeType {
public protocol CofreeType: TermType {
typealias Annotation
typealias Leaf
init(_ annotation: Annotation, _ syntax: Syntax<Self, Leaf>)
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 {}
@ -127,8 +152,8 @@ extension Cofree: CofreeType {}
extension CofreeType where Self.Annotation == Range<String.Index> {
public func JSON(source: String) -> Doubt.JSON {
return unwrap.JSON(
ifLeaf: { _ in .String(source[extract]) },
ifRecur: {
leaf: { _ in .String(source[extract]) },
recur: {
[
"range": [
"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.
///
/// `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.
public enum Free<A, B>: CustomDebugStringConvertible, SyntaxConvertible {
/// The injection of a value of type `B` into the `Syntax` tree.
case Pure(B)
public enum Free<Leaf, Annotation, Value>: CustomDebugStringConvertible {
/// The injection of a value of type `Value` into the `Syntax` tree.
case Pure(Value)
/// 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`.
public init<Term: TermType where Term.LeafType == A>(_ term: Term) {
self = .Roll(term.unwrap.map(Free.init))
/// Construct a `Free` from a `CofreeType` with matching `Leaf` and `Annotation` types, copying the recursive structure of the term in via hylomorphism.
///
/// 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 {
case let .Pure(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.
///
/// 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 {
/// return free.iterate { flattenedSyntax in
/// func size<Leaf, Annotation, Value>(free: Free<Leaf, Annotation, Value>) -> Int {
/// return free.cata { flattenedSyntax in
/// switch flattenedSyntax {
/// case .Leaf:
/// 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/)_.
public func iterate(transform: Syntax<B, A> -> B) -> B {
public func cata(transform: Syntax<Value, Leaf> -> Value) -> Value {
return analysis(
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.
public func reduce(initial: B, combine: (B, B) -> B) -> B {
return iterate {
public func reduce(initial: Value, combine: (Value, Value) -> Value) -> Value {
return cata {
switch $0 {
case .Leaf:
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.
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: +)
}
// MARK: Functor
public func map<C>(@noescape transform: B -> C) -> Free<A, C> {
return analysis(ifPure: { .Pure(transform($0)) }, ifRoll: { .Roll($0.map { $0.map(transform) }) })
public func map<C>(@noescape transform: Value -> C) -> Free<Leaf, Annotation, C> {
return analysis(ifPure: { .Pure(transform($0)) }, ifRoll: { .Roll($0, $1.map { $0.map(transform) }) })
}
// MARK: Monad
public func flatMap<C>(@noescape transform: B -> Free<A, C>) -> Free<A, C> {
return analysis(ifPure: transform, ifRoll: { .Roll($0.map { $0.flatMap(transform) }) })
public func flatMap<C>(@noescape transform: Value -> Free<Leaf, Annotation, C>) -> Free<Leaf, Annotation, C> {
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)))"
}
}
// MARK: SyntaxConvertible
public init(syntax: Syntax<Free, A>) {
self = .Roll(syntax)
}
}
// MARK: - Anamorphism
extension Free {
public static func Introduce(annotation: Annotation)(syntax: Syntax<Free, Leaf>) -> Free {
return Roll(annotation, syntax)
}
/// 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.
public static func ana<Seed>(transform: Seed -> Syntax<Seed, A>)(_ seed: Seed) -> Free {
return (Roll <<< { $0.map(ana(transform)) } <<< transform)(seed)
public static func ana(unfold: Annotation -> Syntax<Annotation, Leaf>)(_ seed: Annotation) -> Free {
return (Introduce(seed) <<< { $0.map(ana(unfold)) } <<< unfold) <| seed
}
}
extension Free where B: PatchType, B.Element == Cofree<A, ()> {
public typealias Term = B.Element
extension Free where Value: PatchType, Value.Element == Cofree<Leaf, ()> {
public typealias Term = Value.Element
public func merge(transform: B -> Term) -> Term {
return map(transform).iterate { Cofree((), $0) }
public func merge(transform: Value -> Term) -> Term {
return map(transform).cata { Cofree((), $0) }
}
public func merge(transform: B -> Term?) -> Term? {
return map(transform).iterate(Free.discardNullTerms)
public func merge(transform: Value -> Term?) -> Term? {
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 {
case let .Leaf(a):
return Cofree((), .Leaf(a))
@ -161,17 +160,17 @@ extension Free where B: PatchType, B.Element == Cofree<A, ()> {
// MARK: - Patch construction
extension Free where B: PatchType {
public static func Replace(before: B.Element, _ after: B.Element) -> Free {
return .Pure(B(replacing: before, with: after))
extension Free where Value: PatchType {
public static func Replace(before: Value.Element, _ after: Value.Element) -> Free {
return .Pure(Value(replacing: before, with: after))
}
public static func Insert(after: B.Element) -> Free {
return .Pure(B(inserting: after))
public static func Insert(after: Value.Element) -> Free {
return .Pure(Value(inserting: after))
}
public static func Delete(before: B.Element) -> Free {
return .Pure(B(deleting: before))
public static func Delete(before: Value.Element) -> Free {
return .Pure(Value(deleting: before))
}
@ -184,46 +183,45 @@ extension Free where B: PatchType {
// MARK: - Equality
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) {
case let (.Pure(a), .Pure(b)):
return ifPure(a, b)
case let (.Roll(a), .Roll(b)):
return Syntax.equals(ifLeaf: ifRoll, ifRecur: equals(ifPure: ifPure, ifRoll: ifRoll))(a, b)
return pure(a, b)
case let (.Roll(annotation1, syntax1), .Roll(annotation2, syntax2)):
return annotation(annotation1, annotation2) && Syntax.equals(leaf: leaf, recur: equals(pure: pure, leaf: leaf, annotation: annotation))(syntax1, syntax2)
default:
return false
}
}
}
public func == <A: Equatable, B: Equatable> (left: Free<A, B>, right: Free<A, B>) -> Bool {
return Free.equals(ifPure: ==, ifRoll: ==)(left, right)
public func == <Leaf: Equatable, Value: Equatable, Annotation: Equatable> (left: Free<Leaf, Annotation, Value>, right: Free<Leaf, Annotation, Value>) -> Bool {
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 {
return Free.equals(ifPure: Patch.equals(Term.equals(==)), ifRoll: ==)(left, right)
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(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
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(
ifPure: {
[ "pure": ifPure($0) ]
},
ifPure: { [ "pure": pure($0) ] },
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

View File

@ -1,7 +1,7 @@
/// An interpreter of `Algorithm`s.
public struct Interpreter<Term: TermType> {
public struct Interpreter<Term: CofreeType> {
/// 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.
///
@ -42,15 +42,21 @@ public struct Interpreter<Term: TermType> {
/// Diff `a` against `b`, if comparable.
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 }
let algorithm: Algorithm<Term, Diff>
let annotations = (a.extract, b.extract)
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)):
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)):
algorithm = .Roll(.ByIndex(a, b, Syntax.Indexed >>> Diff.Roll >>> Algorithm.Pure))
algorithm = .Roll(.ByIndex(a, b, Syntax.Indexed >>> Diff.Introduce(annotations) >>> Algorithm.Pure))
default:
algorithm = .Roll(.Recursive(a, b, Algorithm.Pure))
}
@ -64,12 +70,13 @@ public struct Interpreter<Term: TermType> {
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.
let annotations = (a.extract, b.extract)
switch (a.unwrap, b.unwrap) {
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):
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:
// 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
extension Interpreter where Term.LeafType: Equatable {
extension Interpreter where Term.Leaf: Equatable {
public init(comparable: (Term, Term) -> Bool, cost: Diff -> Int) {
self.init(equal: Term.equals(==), comparable: comparable, cost: cost)
}

View File

@ -10,7 +10,7 @@ import Foundation
import Doubt
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 {
case Number(Double)

View File

@ -130,7 +130,7 @@ extension Patch where A: CustomJSONConvertible {
// 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 {
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.
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>>] {
typealias Diff = 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<Leaf, Annotation, Patch<Term>>
if a.isEmpty { return b.map { .Insert($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 {
switch self {
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
extension Syntax: ArrayLiteralConvertible {
@ -52,14 +80,14 @@ extension Syntax: DictionaryLiteralConvertible {
// MARK: - Equality
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) {
case let (.Leaf(l1), .Leaf(l2)):
return ifLeaf(l1, l2)
return leaf(l1, l2)
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)):
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:
return false
}
@ -67,46 +95,24 @@ extension Syntax {
}
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
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 {
case let .Leaf(a):
return ifLeaf(a)
return leaf(a)
case let .Indexed(a):
return .Array(a.map(ifRecur))
return .Array(a.map(recur))
case let .Keyed(d):
return .Dictionary(Dictionary(elements: d.map { ($0, ifRecur($1)) }))
return .Dictionary(Dictionary(elements: d.map { ($0, recur($1)) }))
}
}
}
// MARK: - Construction
/// 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))
}
}
import Prelude

View File

@ -1,27 +1,27 @@
/// The type of terms.
public protocol TermType {
typealias LeafType
typealias Leaf
var unwrap: Syntax<Self, LeafType> { get }
var unwrap: Syntax<Self, Leaf> { get }
}
extension TermType {
public static func unwrap(term: Self) -> Syntax<Self, LeafType> {
public static func unwrap(term: Self) -> Syntax<Self, Leaf> {
return term.unwrap
}
/// 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.
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)
}
/// 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.
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)
}
@ -44,14 +44,11 @@ extension TermType {
}
extension Cofree: TermType {}
// MARK: - Equality
extension TermType {
public static func equals(leaf: (LeafType, LeafType) -> Bool)(_ a: Self, _ b: Self) -> Bool {
return Syntax.equals(ifLeaf: leaf, ifRecur: equals(leaf))(a.unwrap, b.unwrap)
public static func equals(leaf: (Leaf, Leaf) -> Bool)(_ a: Self, _ b: Self) -> Bool {
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 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)))
@ -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() {
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
@ -50,7 +56,7 @@ final class DiffTests: XCTestCase {
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 {
func testRestrictsComparisons() {
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() {
@ -21,22 +23,22 @@ final class InterpreterTests: XCTestCase {
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 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(.Delete(Term(1, .Leaf("a")))),
Diff(Term(2, .Leaf("b"))),
.Roll((2, 2), .Leaf("b")),
.Pure(.Insert(Term(3, .Leaf("a")))),
.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")))),
Diff(Term(2, .Leaf("b"))),
.Roll((2, 2), .Leaf("b")),
.Pure(.Replace(Term(3, .Leaf("c")), Term(3, .Leaf("a")))),
])

View File

@ -1,5 +1,5 @@
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 b: RangedTerm

View File

@ -12,36 +12,40 @@ final class SESTests: XCTestCase {
}
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() {
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() {
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() {
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() {
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() {
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() {
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 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 b = Term((), .Leaf("b"))
@ -49,11 +53,11 @@ private let c = Term((), .Leaf("c"))
private let d = Term((), .Leaf("d"))
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 {
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)
}
}
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),
"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)
return benchmark("decoding data into string") {
NSString(data: data, encoding: NSUTF8StringEncoding) as String?
}
}
let readFile = { (path: String) -> String? in