Last active
September 16, 2018 14:26
-
-
Save designatednerd/368d48775b6cf71168fb941299ac9927 to your computer and use it in GitHub Desktop.
Chainable Result
This file contains hidden or 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
//: This is based on Vincent Pradeilles' NSSpain 2018 lightning talk. I tried to find a way around using a | |
//: custom operator for this because I haaaaate custom operators, but everything else I tried just led to | |
//: more callback hell. | |
//: | |
//: Custom `infix` operators allow you to pass the left hand side and right hand side of the operator as | |
//: the first and second parameters of a function. I couldn't figure out how to make a function which did that. | |
//: | |
//: Vincent's original approach is outlined here: | |
//: https://github.com/vincent-pradeilles/slides/blob/master/nsspain-2018-solving-callback-hell-with-good-old-function-composition.pdf | |
//: | |
//: Other differences: | |
//: | |
//: - His approach uses ~> as the operator - I use --> because I think it more accurately depicts | |
//: the idea that a function is being passed. Also getting to the tilde key makes my wrist hurt :P. | |
//: - I'm using a `Result` enum instead of a `(Result?, Error?)` tuple because I prefer that for avoiding | |
//: optional weirdness. | |
//: - I added support for throwing `map` functions and for all `filter` functions. | |
//: | |
//: Things I still haven't figured out how to do yet: | |
//: | |
//: - How to make the first function able to take a paramter but not need the completion block explicitly declared | |
//: - Threading | |
import UIKit | |
enum Result<T> { | |
case success(T) | |
case error(Error) | |
} | |
typealias CompletionHandler<T> = (Result<T>) -> Void | |
typealias CompoundHandler<T> = (CompletionHandler<T>) -> Void | |
func getInt(completion: CompletionHandler<Int>) { | |
completion(.success(42)) | |
} | |
func divideInt(_ int: Int, completion: CompletionHandler<Int>) { | |
let divided = int / 2 | |
completion(.success(divided)) | |
} | |
enum StringConversionError: Error { | |
case notANumber | |
} | |
func convertToString(_ int: Int, completion: CompletionHandler<String>) { | |
let formatter = NumberFormatter() | |
formatter.numberStyle = .spellOut | |
guard let formatted = formatter.string(from: NSNumber(value: int)) else { | |
completion(.error(StringConversionError.notANumber)) | |
return | |
} | |
completion(.success(formatted)) | |
} | |
func toArray(int: Int) -> [Int] { | |
return [int, int + 1, int + 2] | |
} | |
func isEven(int: Int) -> Bool { | |
return (int % 2) == 0 | |
} | |
enum FilterError: Error { | |
case emptyArray | |
} | |
func first(in array: [Int]) throws -> Int { | |
guard let first = array.first else { | |
throw FilterError.emptyArray | |
} | |
return first | |
} | |
// YE OLDE CALLBACK HELL | |
getInt(completion: { result in | |
switch result { | |
case .success(let int): | |
divideInt(int) { secondResult in | |
switch secondResult { | |
case .success(let divided): | |
convertToString(divided) { thirdResult in | |
switch thirdResult { | |
case .success(let string): | |
print(string) | |
case .error(let error): | |
debugPrint("Error: \(error)") | |
} | |
} | |
case .error(let error): | |
debugPrint("Error: \(error)") | |
} | |
} | |
case .error(let error): | |
debugPrint("Error: \(error)") | |
} | |
}) | |
// Should print: "twenty-one" | |
// CHAINING | |
infix operator -->: MultiplicationPrecedence | |
func --> <T, U>(_ firstFunction: @escaping CompoundHandler<T>, | |
_ secondFunction: @escaping ((T, CompletionHandler<U>) -> Void)) -> CompoundHandler<U> { | |
return { completion in | |
firstFunction { result1 in | |
switch result1 { | |
case .success(let item): | |
secondFunction(item, { result2 in | |
completion(result2) | |
}) | |
case .error(let error): | |
completion(.error(error)) | |
} | |
} | |
} | |
} | |
// YE NEW LESS HELLISH CALLBACK | |
let chain1 = getInt --> | |
divideInt --> | |
convertToString | |
chain1({ result in | |
switch result { | |
case .success(let item): | |
print("Chain 1 Got: \(type(of: item)) - \(item)") | |
case .error(let error): | |
debugPrint("Chain 1 Error \(error)") | |
} | |
}) | |
// Should print: "Chain 1 Got: String - twenty-one" | |
// MAPPING | |
func --> <T, U>(_ firstFunction: @escaping CompoundHandler<T>, | |
_ transformFunction: @escaping (T) throws -> U) -> CompoundHandler<U> { | |
return { completion in | |
firstFunction { result in | |
switch result { | |
case .success(let item): | |
do { | |
let transformed = try transformFunction(item) | |
completion(.success(transformed)) | |
} catch let error { | |
completion(.error(error)) | |
} | |
case .error(let error): | |
completion(.error(error)) | |
} | |
} | |
} | |
} | |
// FILTERING | |
func --> <T>(_ firstFunction: @escaping CompoundHandler<[T]>, | |
_ filterFunction: @escaping (T) throws -> Bool) -> CompoundHandler<[T]> { | |
return { completion in | |
firstFunction { result in | |
switch result { | |
case .success(let array): | |
do { | |
let filtered = try array.filter(filterFunction) | |
completion(.success(filtered)) | |
} catch let error { | |
completion(.error(error)) | |
} | |
case .error(let error): | |
completion(.error(error)) | |
} | |
} | |
} | |
} | |
// YE EVEN CALLBACKIER CALLBACK | |
let chain2 = getInt --> // Gets the integer | |
divideInt --> // Divides it | |
toArray --> // Maps to an array | |
isEven --> // Filters array by even numbers | |
first --> // Grabs first number in array | |
convertToString // Converts that first number to string | |
chain2({ result in | |
switch result { | |
case .success(let item): | |
print("Chain 2 Got: \(type(of: item)) - \(item)") | |
case .error(let error): | |
debugPrint("Chain 2 Error \(error)") | |
} | |
}) | |
// Should print: "Chain 2 Got: String - twenty-two" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment