When we implement complex scenarios involving asynchronous code, for instance when chaining network calls, we quickly end up with a code filled closures nested into one another. This kind of code is often dubbed « callback hell », because of how hard it is to maintain and reason about.
In the last years, many libraries, such as RxSwift or PromiseKit have been released, with the purpose of tackling this issue. While they work like a charm, it’s actually possible to solve a callback hell situation with a much simpler trick: a good old function composition.
While we are accustomed to function composition in the realm of synchronous functions, we tend to forget this operation is also possible with asynchronous ones, which is a shame because it can really help in simplifying complex code.
8. service1 { result, _ in
guard let result = result else { return }
service2(arg: result, { result, _ in
guard let result = result else { return }
service3(arg: result, { result, _ in
guard let result = result else { return }
print(result) // Result: 42
})
})
}
28. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
29. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
30. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
31. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
32. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
33. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
34. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
35. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
36. func ~> <T, U>(_ first: @escaping (CompletionHandler<T>) -> Void,
_ transform: @escaping (T) -> U)
-> (CompletionHandler<U>) -> Void {
return { completion in
first({ result, error in
guard let result = result else { completion(nil, error); return }
completion(transform(result), nil)
})
}
}
37. let chainedWithMap = service1
~> { int in return String(int / 2) }
~> service3
chainedWithMap({ result, _ in
guard let result = result else { return }
print(result) // Prints: 21
})
38. let chainedWithMap = service1
~> { int in return String(int / 2) }
~> service3
chainedWithMap({ result, _ in
guard let result = result else { return }
print(result) // Prints: 21
})