HoneyBee 3.0.0

HoneyBee 3.0.0

TestsTested
LangLanguage SwiftSwift
License MIT
ReleasedLast Release Oct 2020
SPMSupports SPM

Maintained by Alex Lynch.



HoneyBee 3.0.0

  • By
  • Alex Lynch

HoneyBee


Introduction

HoneyBee is a Swift promises library to increase the expressiveness of asynchronous and concurrent programming. HoneyBee design follows a few principles:

  • Show Me. Concurrent code should look like the structure that it implements (see Examples below)
  • Bring Your Own Code. HoneyBee works with your asynchronous and synchronous functions, as they are today, with no modifications. (Usually.)
  • Safe By Default. HoneyBee enforces proper error handling techniques - while also reducing programmer burden.

Documentation

View project documentation here.

Quick Examples

Example: Show Me.

HoneyBee.start { root in
    root.setErrorHandler(errorHandlingFunc)
        .chain(func1)
        .branch { stem in
            stem.chain(func3)
                .chain(func4)
            +
            stem.chain(func5)
                .chain(func6)
        }
        .chain(func7)
}

In the above recipe, func1 will be called first. Then the result of func1 will be passed to func3 and func5 in parallel. func4 will be called after func3 has finished and will be passed the result of func3. Likewise, func6 will be called after func5 has finished and will be passed the result of func5. When both func4 and func6 have finished, their results will be combined into a tuple and passed to func7. If any of the functions throws or asynchronously responds with an Error, then errorHandlingFunc will be invoked with the error as an argument.

Example: BYOC (Bring Your Own Code)

func func1(completion: ([String]?, Error?) -> Void) {...}
func func2(string: String) throws -> Int {...}
func func3(int: Int, completion: (Error?) -> Void) {...}
func func4(int: Int, completion: (Result<String, Error>) -> Void) {...}
func func5(strings: [String], completion: () -> Void) {...}
func successFunc(strings: [String]) {...}
HoneyBee.start { root in
    root.setErrorHandler(errorHandler)
        .chain(func1)
        .map { elem in
            elem.chain(func2)
                .chain(func3)
                .chain(func4)
        }
        .chain(func5)
        .chain(successFunc)
}

In the above recipe we see six of HoneyBee's supported function signatures. func1 is an Objective-C style errorring async callback. func2 is a synchronous Swift throwing function. func3 completes with an optional error but does not generate a new value. HoneyBee forwards the inbound value automatically. func4 is a Swift style, generic enum-based result which may contain a value or may contain an error. func5 is asynchronous but cannot error (UI animations fit this category). And successFunc is a simply, synchronous non-errorring function.

HoneyBee supports 34 distinct function signatures.

Example: Safe By Default

One of the many problems with the "pyramid of doom" is that error handling is hard to get right.

func processImageData1(completionBlock: (result: Image?, error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        loadWebResource("imagedata.dat") { imageResource, error in
            decodeImage(dataResource, imageResource) { imageTmp, error in
                dewarpAndCleanupImage(imageTmp) { imageResult in
                    completionBlock(imageResult, nil)
                }
            }
        }
    }
}

The above naive, "happy path" code has no error handling. Let's add the most principled form of handling now:

func processImageData2(completionBlock: (result: Image?, error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            completionBlock(nil, error)
            return
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                completionBlock(nil, error)
                return
            }
            decodeImage(dataResource, imageResource) { imageTmp, error in
                guard let imageTmp = imageTmp else {
                    completionBlock(nil, error)
                    return
                }
                dewarpAndCleanupImage(imageTmp) { imageResult in
                    guard let imageResult = imageResult else {
                        completionBlock(nil, error)
                        return
                    }
                    completionBlock(imageResult, nil)
                }
            }
        }
    }
}

Not very pretty, right? And there's still issues here. This form of processImageData has made its contract correctness dependent on the contract correctness of all of the invoked asynchronous methods. What happens if one of the methods fails to call its completion? Or calls back more than once? What happens if a method calls the completion, but with two nil values? HoneyBee handles each of these issues for you, so that your method's correctness is not dependent on the correctness of any dependency method. Let's take a look at the Honeybee form:

func processImageData3(completionBlock: (result: Image?, error: Error?) -> Void) {
    HoneyBee.start { root in
        root.setErrorHandler { completionBlock(nil, $0)}
            .branch { stem in
                stem.chain(loadWebResource =<< "dataprofile.txt")
                +
                stem.chain(loadWebResource =<< "imagedata.dat")
            }
            .chain(decodeImage)
            .chain(dewarpAndCleanupImage)
            .chain{ completionBlock($0, nil) }
    }
}

So much cleaner right? And Bonus Points, the HoneyBee implementation allows us to parallelize the first two async calls to loadWebResource, so this form has better performance than the others too. Groovy.

(If you're wondering about the =<< operator it's pronounced bind. It performs a partial function application, "binding" the argument to the function. See the API docs for more details.)

Error Diagnostics

Diagnosing problems in misbehaving concurrent code is really hard right? Not with HoneyBee. Consider the following:

func handleError(_ errorContext: ErrorContext) {
    print(errorContext)
}
func stringToInt(string: String, callback: (Result<Int, Error>) -> Void) {
    if let int = Int(string) {
        callback(.success(int))
    } else {
        let error = NSError(domain: "couldn't convert string to int", code: -2, userInfo: ["string:": string])
        callback(.failure(error))
    }
}
HoneyBee.start { root in
    root.setErrorHandler(handleError)
        .insert(7)
        .chain(String.init)              // produces "7"
        .chain(String.append =<< "dog")  // produces "7dog"
        .chain(stringToInt)              // errors
        .chain(successFunc)              // not reached
}

prints

subject = "7dog"
file = "/Users/HoneyBee/Tests/ErrorHandlingTests.swift"
line = 172
internalPath = 5 values {
  [0] = "start: /Users/HoneyBee/Tests/ErrorHandlingTests.swift:167"
  [1] = "chain: /Users/HoneyBee/Tests/ErrorHandlingTests.swift:169 insert"
  [2] = "chain: /Users/HoneyBee/Tests/ErrorHandlingTests.swift:170 (Int) -> String"
  [3] = "chain: /Users/HoneyBee/Tests/ErrorHandlingTests.swift:171 (String) -> String"
  [4] = "chain: /Users/HoneyBee/Tests/ErrorHandlingTests.swift:172 (String, (FailableResult<Int>) -> ()) -> ()"
}

HoneyBee pinpoints the file and line where the recipe errored, along with the path which was taken to arrive at that function, and the inbound "subject" value. In most cases this reduces your diagnostic search process to a single function.

Multiple Queues

By default HoneyBee performs all functions on the global background queue. What if you need to work on the main queu?

HoneyBee.start(on: DispatchQueue.main) { root in
    root.setErrorHandler(handleError)
        .chain(func1)  // performed on main queue
        .chain(func2)  // same
}

Easy right? Need to change queues? What about NSManagedObjectContexts?

HoneyBee.start(on: DispatchQueue.main) { root in
    root.setErrorHandler(handleError)
        .chain(func1)  // performed on main queue
        .setBlockPerformer(DispatchQueue.global())
        .chain(func2)  // performed on global background queue 
        .chain(func3)  // performed on global background queue 
        .setBlockPerformer(myMOC)
        .chain(func4)  // performed on myMOC's internal queue. 
}

HoneyBee puts you in complete control of what queue will invoke your functions. This remains true even if the functions themselves call back on different queues than they were invoked from.

Wrap Up

So that's HoneyBee. Expressive, easy, and safe. Concurrency the way it should be. If you have any questions, contact me.