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

Merge branch 'master' into diff-ui-shows-original-sources-for-unchanged-ranges-as-well

This commit is contained in:
Rob Rix 2015-10-23 12:02:51 -04:00
commit 2213606727
6 changed files with 44 additions and 21 deletions

View File

@ -124,9 +124,9 @@ extension CofreeType {
///
/// 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(unfold: Annotation -> Syntax<Annotation, Leaf>)(_ seed: Annotation) -> Self {
return (Introduce(seed) <<< { $0.map(coiterate(unfold)) } <<< unfold) <| seed
/// 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.

View File

@ -13,6 +13,14 @@ public enum Free<Leaf, Annotation, Value>: CustomDebugStringConvertible {
indirect case Roll(Annotation, Syntax<Free, Leaf>)
/// 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: Value -> C, @noescape ifRoll: (Annotation, Syntax<Free, Leaf>) -> C) -> C {
switch self {
case let .Pure(b):
@ -26,14 +34,14 @@ public enum Free<Leaf, Annotation, Value>: CustomDebugStringConvertible {
///
/// `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 `Value`.
/// 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<Leaf, Annotation, Value>(free: Free<Leaf, Annotation, Value>) -> Int {
/// return free.iterate { flattenedSyntax in
/// return free.cata { flattenedSyntax in
/// switch flattenedSyntax {
/// case .Leaf:
/// return 1
@ -45,19 +53,19 @@ public enum Free<Leaf, Annotation, Value>: CustomDebugStringConvertible {
/// }
/// }
///
/// 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<Value, Leaf> -> Value) -> Value {
public func cata(transform: Syntax<Value, Leaf> -> Value) -> Value {
return analysis(
ifPure: id,
ifRoll: { $1.map { $0.iterate(transform) } } >>> 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: Value, combine: (Value, Value) -> Value) -> Value {
return iterate {
return cata {
switch $0 {
case .Leaf:
return initial
@ -112,8 +120,8 @@ extension 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.
public static func coiterate(unfold: Annotation -> Syntax<Annotation, Leaf>)(_ seed: Annotation) -> Free {
return (Introduce(seed) <<< { $0.map(coiterate(unfold)) } <<< unfold) <| seed
public static func ana(unfold: Annotation -> Syntax<Annotation, Leaf>)(_ seed: Annotation) -> Free {
return (Introduce(seed) <<< { $0.map(ana(unfold)) } <<< unfold) <| seed
}
}
@ -122,11 +130,11 @@ extension Free where Value: PatchType, Value.Element == Cofree<Leaf, ()> {
public typealias Term = Value.Element
public func merge(transform: Value -> Term) -> Term {
return map(transform).iterate { Cofree((), $0) }
return map(transform).cata { Cofree((), $0) }
}
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?, Leaf>) -> Term? {

View File

@ -42,7 +42,10 @@ public struct Interpreter<Term: CofreeType> {
/// Diff `a` against `b`, if comparable.
private func recur(a: Term, _ b: Term) -> Diff? {
if equal(a, b) { return reiterate(Diff.Roll, Cofree.eliminate)(Term.zip(a, 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>

View File

@ -38,7 +38,7 @@ public enum Syntax<Recur, A>: CustomDebugStringConvertible {
/// 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 `TermType.cata`, `Free.iterate`) and an anamorphism (see also `Free.ana`, `CofreeType.coiterate`), 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`).
/// 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`.
///
@ -49,13 +49,13 @@ public func hylo<A, B, Leaf>(down: Syntax<B, Leaf> -> B, _ up: A -> Syntax<A, Le
/// 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 `TermType.cata`, `Free.iterate`) and an anamorphism (see also `Free.ana`, `CofreeType.coiterate`), 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`).
/// 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`.
///
/// `reiterate` can be used with arbitrary functors which can eliminate to and introduce with `Annotation` & `Syntax` pairs.
public func reiterate<A, B, Leaf, Annotation>(down: (Annotation, Syntax<B, Leaf>) -> B, _ up: A -> (Annotation, Syntax<A, Leaf>)) -> A -> B {
return up >>> { ($0, $1.map(reiterate(down, up))) } >>> down
/// `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
}

View File

@ -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

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
}
}
}