Last active
February 3, 2018 23:03
-
-
Save elm4ward/3c03db368cb2e74677a76e1693cddec4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ---------------------------------------------------------------------------------------------------- | |
// ArrayZipper Comonads in Swift | |
// | |
// http://elm4ward.github.io/swift/functional/comonad/2016/03/30/monads-comonads.html | |
// | |
// 1. Array extensions | |
// 2. ArrayZipperError | |
// 3. ArrayZipper | |
// 4. ArrayZipper extensions | |
// 5. custom operators | |
// 6. extend methods | |
// 7. finally, the example | |
// ---------------------------------------------------------------------------------------------------- | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 1. Array Extension | |
// | |
// Some initial setup functions necessary | |
// ---------------------------------------------------------------------------------------------------- | |
extension Array { | |
/// take the first i Elements | |
func take(i: Int) -> SubSequence { | |
let drop = count - i | |
guard drop >= 0 && drop < count else { return dropLast(0) } | |
return dropLast(drop) | |
} | |
/// take the last i Elements | |
func takeLast(i: Int) -> SubSequence { | |
let drop = count - i | |
guard drop >= 0 && drop < count else { return dropFirst(0) } | |
return dropFirst(drop) | |
} | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 2. ArrayZipperError | |
// | |
// mostly used to avoid an optional 'from' method in 'ArrayZipper' | |
// ---------------------------------------------------------------------------------------------------- | |
enum ArrayZipperError: ErrorType { | |
case emptyList | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 3. ArrayZipper | |
// | |
// the Zipper. | |
// Could also be build be an Array and Int pointing to the Element. | |
// ---------------------------------------------------------------------------------------------------- | |
enum ArrayZipper<T> { | |
typealias Structure = (head: [T], focus: T, tail: [T]) | |
case zipper([T], T, [T]) | |
/// ArrayZipper from a Array of S | |
static func from<S>(a: [S]) throws -> ArrayZipper<S> { | |
guard let focus = a.first else { throw ArrayZipperError.emptyList } | |
return .zipper([], focus, Array(a.dropFirst())) | |
} | |
/// Get the structured values | |
var structure: Structure { | |
switch self { | |
case let .zipper(h, f, t): return (head: h, focus: f, tail: t) | |
} | |
} | |
/// Get all as Array | |
var all: [T] { | |
switch self { | |
case let .zipper(h, f, t): return h + [f] + t | |
} | |
} | |
/// Get current position of zipper | |
var position: Int { | |
switch self { | |
case let .zipper(h, _, _): return h.count | |
} | |
} | |
/// Move the zipper to next position | |
func moveTo(position: Int) -> ArrayZipper<T>? { | |
let ts = all | |
let ft = Array(ts.dropFirst(position)) | |
guard let f = ft.first else { return nil } | |
return .zipper(Array(ts.dropLast(ts.count - position)), f, Array(ft.dropFirst())) | |
} | |
/// Get the zipper on the left side | |
func left() -> ArrayZipper<T>? { | |
return moveTo(position.predecessor()) | |
} | |
/// Get the zipper on the right side | |
func right() -> ArrayZipper<T>? { | |
return moveTo(position.successor()) | |
} | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 4. ArrayZipper 'Functor' | |
// | |
// yes, a zipper is a Functor and therefore is has to have a map method | |
// ---------------------------------------------------------------------------------------------------- | |
extension ArrayZipper { | |
/// map each value in the structure of the zipper | |
func map<A>(f: T -> A) -> ArrayZipper<A> { | |
switch self { | |
case let .zipper(head, focus, tail): | |
return .zipper(head.lazy.map(f), f(focus), tail.lazy.map(f)) | |
} | |
} | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 4. ArrayZipper 'Comonad' | |
// | |
// of course, a zipper is a Comonad. | |
// ---------------------------------------------------------------------------------------------------- | |
extension ArrayZipper { | |
/// get the focus of the current structure | |
func extract() -> T { | |
return structure.focus | |
} | |
/// duplicate the structure and move at each position | |
/// will build a nested ArrayZipper | |
func duplicate() -> ArrayZipper<ArrayZipper<T>> { | |
let zippers = (0..<all.count).flatMap(moveTo) | |
return try! ArrayZipper.from(zippers).moveTo(position)! | |
} | |
/// get the duplicated structure and map with extend function which will 'reduce' the | |
/// ArrayZipper<ArrayZipper<T>> to ArrayZipper<S> | |
func extend<S>(f: ArrayZipper<T> -> S) -> ArrayZipper<S> { | |
return duplicate().map(f) | |
} | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 4. Comonad extend operator | |
// | |
// A custom operator to call extend on a comonad. inspired by the original haskell operator | |
// ---------------------------------------------------------------------------------------------------- | |
infix operator ->> { precedence 140 associativity left } | |
/// easy: call the extend method | |
func ->> <A, B>(lhs: ArrayZipper<A>, rhs: ArrayZipper<A> -> B) -> ArrayZipper<B> { | |
return lhs.extend(rhs) | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 5. CoKleisli operator | |
// | |
// I dont even wanna talk about that name. | |
// A custom operator to combine extends for a comonad. inspired by the original haskell operator | |
// ---------------------------------------------------------------------------------------------------- | |
infix operator =>= { precedence 150 associativity left } | |
/// great: combine 2 extend methods for reuse | |
func =>= <A, B, C>(lhs: ArrayZipper<A> -> B, rhs: ArrayZipper<B> -> C) -> ArrayZipper<A> -> C { | |
return { $0.extend(lhs).extend(rhs).extract() } | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 6. Extends Methods | |
// | |
// Lets setup a couple of functions which examine the zipper and give us a value | |
// ---------------------------------------------------------------------------------------------------- | |
func average(a: [Int]) -> Int { | |
guard !a.isEmpty else { return 0 } | |
return a.reduce(0, combine: +) / a.count | |
} | |
/// gets the average of the elements in the radius of the focus | |
func avg(radius radius: Int) -> ArrayZipper<Int> -> Int { | |
return { z in | |
let structure = z.structure | |
let ax = structure.head.takeLast(radius) + [structure.focus] + structure.tail.take(radius) | |
return average(Array(ax)) | |
} | |
} | |
enum Judge { | |
case Better, Worse | |
} | |
/// tells wether the focus is better or worse than its predecessor | |
func judge<C: Comparable>(a: ArrayZipper<C>) -> Judge { | |
let structure = a.structure | |
guard let last = structure.head.last else { return .Better } | |
return structure.focus >= last ? .Better : .Worse | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 7. Example | |
// | |
// finally the use case | |
// ---------------------------------------------------------------------------------------------------- | |
/// the list of values | |
let values = [100, 160, 120, 223, 221, 180, 200, 170, 140, 190, 180, 170, 123, 101, 80, 90, 140, 190, 222] | |
/// the zipper | |
let valuesZipper: ArrayZipper<Int> = try! .from(values) | |
// wow ... so easy getting the average of each element in combination with its 3 siblings before and after | |
let avgs = valuesZipper ->> avg(radius: 3) | |
/// combine into a extend method that judges the averages | |
let didAverageGetBetter = avg(radius: 3) =>= judge | |
// wow ... so easy to see wether the average did get better or worse | |
let judges = valuesZipper ->> didAverageGetBetter |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment