Skip to content

Commit

Permalink
Tools improving asynchronous quality of life.
Browse files Browse the repository at this point in the history
* Adds `asyncFlatMap` for when `transform` has async components.
* Adds `flatRoute` for converting "completion blocks" (AKA "continuations") between result types.
* Adds `asyncFlatRoute` for doing that when the `adaptor` has async components.
* Adds simple `isSuccess` and `isFailure` predicates for avoiding case statements.
* Also deprecates Result initializers. See also: Issue #4.
* Sprinkles in some documentation.
  • Loading branch information
jemmons committed Aug 16, 2018
1 parent 3722c66 commit dd4eb5e
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 1 deletion.
2 changes: 1 addition & 1 deletion SessionArtist/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>0.6.4</string>
<string>0.6.5</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
Expand Down
66 changes: 66 additions & 0 deletions SessionArtist/Result.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import Foundation



/**
An `Either` type wrapping either abitrary value of type `T` or an `Error`.
*/
public enum Result<T> {
case success(T)
case failure(Error)


@available(*, deprecated, message: "Considered harmful. `Result(MyError)` will unexpectedly return a `Result<MyError>.success`." )
public init(_ value: T) {
self = .success(value)
}


@available(*, deprecated, message: "Considered harmful. `Result(MyError)` will unexpectedly return a `Result<MyError>.success`." )
public init(_ error: Error) {
self = .failure(error)
}
Expand All @@ -20,6 +25,26 @@ public enum Result<T> {


public extension Result {
var isSuccess: Bool {
switch self {
case .success:
return true
case .failure:
return false
}
}


var isFailure: Bool {
return !isSuccess
}


/**
If `self` is a `success`, unwraps the value and maps it to a new value via `transform` which is, itself, wrapped in a new `success`. If `self` is a `failure`, reutrns that failure directly without evaluating `transform`.
* note: Implicit here is the idea that `transform` will always succeed. If it can fail, use `flatMap(transform:)` instead so that an error can be thrown or a `failure` returned.
* seeAlso: `flatMap(transform:)`
*/
func map<U>(transform: (T)->U) -> Result<U> {
switch self {
case .success(let t):
Expand All @@ -30,6 +55,10 @@ public extension Result {
}


/**
If `self` is a `success`, unwraps the value and maps it to a new `success` *or* `failure` via `transform`. As a convenience, any errors thrown during `transform` will be mapped to a `failure`. If `self` is already a `failure`, reutrns that failure directly without evaluating `transform`.
* seeAlso: `map(transform:)`, `asyncFlatMap(asyncTransform:completion:)`
*/
func flatMap<U>(transform: (T) throws -> Result<U>) -> Result<U> {
switch self {
case .success(let t):
Expand All @@ -44,6 +73,24 @@ public extension Result {
}


/**
If `self` is a `success`, unwraps the value and maps it to a new `success` *or* `failure` via `transform`, calling `completion` with the result. If `self` is already a `failure`, passes that failure directly `completion` without evaluating `transform`.
* note: the transform function is, itself, asynchronous in the CPS-style.
* seeAlso: `flatMap(transform:)`
*/
func asyncFlatMap<U>(asyncTransform: (T, @escaping (Result<U>)->Void)->Void, completion: @escaping (Result<U>)->Void) {
switch self {
case .success(let t):
asyncTransform(t) { u in
completion(u)
}
case .failure(let e):
completion(.failure(e))
}
}


/// If `self` is a `success`, unwrap the value and return it. Otherwise, throw the `failure`'s error.
func resolve() throws -> T {
switch self {
case .success(let t):
Expand All @@ -52,4 +99,23 @@ public extension Result {
throw e
}
}


/// Compose two `Result` continuations. Unlike the generic `route(continuation:adaptor)`, this version `flatMap`s over results with `adaptor` so we don't have to deal with `failure` branches.
static func flatRoute<U>(continuation: @escaping (Result<U>)->Void, adaptor: @escaping (T)->Result<U>) -> (Result<T>)->Void {
return { t in
continuation(t.flatMap(transform: adaptor))
}
}


/**
Compose two `Result` continuations via an adaptor that is, itself, asynchronous in the CPS-style. Necessary for adaptors that need to call async routines.
* seeAlso: `flatRoute(continuation:adaptor:)
*/
static func asyncFlatRoute<U>(continuation: @escaping (Result<U>)->Void, asyncAdaptor: @escaping (T, @escaping (Result<U>)->Void)->Void) -> (Result<T>)->Void {
return { t in
t.asyncFlatMap(asyncTransform: asyncAdaptor, completion: continuation)
}
}
}
200 changes: 200 additions & 0 deletions SessionArtistTests/ResultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ class ResultTests: XCTestCase {
}


func testPredicates() {
XCTAssert(success.isSuccess)
XCTAssert(failure.isFailure)
XCTAssertFalse(success.isFailure)
XCTAssertFalse(failure.isSuccess)
}


func testInitializer() {
switch Result("foo") {
case .success("foo"):
Expand Down Expand Up @@ -83,6 +91,66 @@ class ResultTests: XCTestCase {
}


func testAsyncFlatMapToSuccess() {
let expectedSuccessFromSuccess = expectation(description: "Waiting for successful result")
let expectedFailureFromFailure = expectation(description: "Waiting for failure result")

let transform: (Int, (Result<String>)->Void)->Void = { i, completion in
completion(.success(String(i)))
}

success.asyncFlatMap(asyncTransform: transform) { res in
switch res {
case .success("42"):
expectedSuccessFromSuccess.fulfill()
default:
XCTFail()
}
}

failure.asyncFlatMap(asyncTransform: transform) { res in
switch res {
case .failure(E.original):
expectedFailureFromFailure.fulfill()
default:
XCTFail()
}
}

wait(for: [expectedSuccessFromSuccess, expectedFailureFromFailure], timeout: 0)
}


func testAsyncFlatMapToFailure() {
let expectedFailureFromSuccess = expectation(description: "Waiting for new failure")
let expectedFailureFromFailure = expectation(description: "Waiting for originnal failure")

let transform: (Int, (Result<String>)->Void)->Void = { i, completion in
completion(.failure(E.new))
}

success.asyncFlatMap(asyncTransform: transform) { res in
switch res {
case .failure(E.new):
expectedFailureFromSuccess.fulfill()
default:
XCTFail()
}
}

failure.asyncFlatMap(asyncTransform: transform) { res in
switch res {
case .failure(E.original):
expectedFailureFromFailure.fulfill()
default:
XCTFail()
}
}

wait(for: [expectedFailureFromSuccess, expectedFailureFromFailure], timeout: 0)
}


func testMap() {
switch success.map(transform: { String($0) }) {
case .success("42"):
Expand Down Expand Up @@ -134,4 +202,136 @@ class ResultTests: XCTestCase {
XCTFail()
}
}


func testFlatRouteToSuccess() {
let expectedSuccessFromSuccess = expectation(description: "Waiting for success.")
let expectedFailureFromFailure = expectation(description: "Waiting for failure.")

let successContinuation: (Result<String>)->Void = { res in
switch res {
case .success("42"):
expectedSuccessFromSuccess.fulfill()
default:
XCTFail()
}
}

let failureContinuation: (Result<String>)->Void = { res in
switch res {
case .failure(E.original):
expectedFailureFromFailure.fulfill()
default:
XCTFail()
}
}

let adaptor: (Int)->Result<String> = { i in
return .success(String(i))
}

Result.flatRoute(continuation: successContinuation, adaptor: adaptor)(success)
Result.flatRoute(continuation: failureContinuation, adaptor: adaptor)(failure)

wait(for: [expectedSuccessFromSuccess, expectedFailureFromFailure], timeout: 0)
}


func testFlatRouteToFailure() {
let expectedFailureFromSuccess = expectation(description: "Waiting for failure from success.")
let expectedFailureFromFailure = expectation(description: "Waiting for failure.")

let successContinuation: (Result<String>)->Void = { res in
switch res {
case .failure(E.new):
expectedFailureFromSuccess.fulfill()
default:
XCTFail()
}
}

let failureContinuation: (Result<String>)->Void = { res in
switch res {
case .failure(E.original):
expectedFailureFromFailure.fulfill()
default:
XCTFail()
}
}

let adaptor: (Int)->Result<String> = { i in
return .failure(E.new)
}

Result.flatRoute(continuation: successContinuation, adaptor: adaptor)(success)
Result.flatRoute(continuation: failureContinuation, adaptor: adaptor)(failure)

wait(for: [expectedFailureFromSuccess, expectedFailureFromFailure], timeout: 0)
}


func testAsyncFlatRouteToSuccess() {
let expectedSuccessFromSuccess = expectation(description: "Waiting for success.")
let expectedFailureFromFailure = expectation(description: "Waiting for failure.")

let successContinuation: (Result<String>)->Void = { res in
switch res {
case .success("42"):
expectedSuccessFromSuccess.fulfill()
default:
XCTFail()
}
}

let failureContinuation: (Result<String>)->Void = { res in
switch res {
case .failure(E.original):
expectedFailureFromFailure.fulfill()
default:
XCTFail()
}
}

let adaptor: (Int, (Result<String>)->Void)->Void = { i, completion in
completion(.success(String(i)))
}

Result.asyncFlatRoute(continuation: successContinuation, asyncAdaptor: adaptor)(success)
Result.asyncFlatRoute(continuation: failureContinuation, asyncAdaptor: adaptor)(failure)

wait(for: [expectedSuccessFromSuccess, expectedFailureFromFailure], timeout: 0)
}


func testAsyncFlatRouteToFailure() {
let expectedFailureFromSuccess = expectation(description: "Waiting for failure from success.")
let expectedFailureFromFailure = expectation(description: "Waiting for failure.")

let successContinuation: (Result<String>)->Void = { res in
switch res {
case .failure(E.new):
expectedFailureFromSuccess.fulfill()
default:
XCTFail()
}
}

let failureContinuation: (Result<String>)->Void = { res in
switch res {
case .failure(E.original):
expectedFailureFromFailure.fulfill()
default:
XCTFail()
}
}

let adaptor: (Int, (Result<String>)->Void)->Void = { i, completion in
completion(.failure(E.new))
}

Result.asyncFlatRoute(continuation: successContinuation, asyncAdaptor: adaptor)(success)
Result.asyncFlatRoute(continuation: failureContinuation, asyncAdaptor: adaptor)(failure)

wait(for: [expectedFailureFromSuccess, expectedFailureFromFailure], timeout: 0)
}
}

0 comments on commit dd4eb5e

Please sign in to comment.