Fernando Martín Ortiz
Posted on March 21, 2021
This has been a long and busy week, but I didn't want to skip my weekly article here on Dev.to, so let me show you something I like to do when I'm bored: I open a Playground and create a simple promise implementation or a networking layer.
Let me introduce to you the minimum and most elegant promises implementation I've created so far.
The code is here:
struct Promise<T> {
typealias ResultType = Result<T, Error>
typealias ResultObserver = (ResultType) -> Void
typealias CreatorFunction = (@escaping ResultObserver) -> Void
private let creatorFunction: CreatorFunction
init(creatorFunction: @escaping CreatorFunction) {
self.creatorFunction = creatorFunction
}
func then<E>(_ f: @escaping (T) -> E) -> Promise<E> {
return Promise<E> { observer in
self.creatorFunction { r in
observer(r.map(f))
}
}
}
static func all(_ promises: [Promise<T>], on queue: DispatchQueue = .main) -> Promise<[T]> {
return Promise<[T]> { observer in
let group = DispatchGroup()
var values = [T]()
var hasFailed = false
for promise in promises {
group.enter()
promise.then { r in
switch r {
case let .success(value):
values.append(value)
case let .failure(error):
observer(.failure(error))
hasFailed = true
}
group.leave()
}
}
group.notify(queue: queue) {
guard !hasFailed else {
return
}
observer(.success(values))
}
}
}
func then<E>(_ f: @escaping (T) -> Promise<E>) -> Promise<E> {
return Promise<E> { observer in
self.creatorFunction { firstResult in
switch firstResult {
case .success(let successResult):
f(successResult).creatorFunction { transformedResult in
observer(transformedResult)
}
case .failure(let error):
observer(.failure(error))
}
}
}
}
@discardableResult func then(_ f: @escaping (ResultType) -> Void) -> Self {
creatorFunction { r in f(r) }
return self
}
}
What is a Promise?
A Promise
, or Future
in Combine
, represents a value that isn't there yet, but you know that it will be there at some point in the future.
For example, if you're performing a network request, a Promise
will tell you "ok, so you've done a network request. I don't have the response yet and it will probably take a couple of seconds, but tell me what you'd like to do with it, and I will do it for you when I have the response". Basically, that's it.
What is the difference with callbacks?
You've probably seen this picture a million times
This is javascript in this case, but it works also for Swift. When doing callbacks, the biggest problem you'll have is that chaining async operations will be painful. I've written in the past about running async operations in parallel. Using DispatchGroup
is a way to solve the problems of fixing when operations need to be executed in parallel and are performed async.
Promises can also do that, leveraging GCD mechanisms under the hood. Plus, they solve the problem of chaining sequences of async operations.
Promises scale much much better than callbacks, and are relatively simple to use once you get used to its idioms.
How does this work?
In the implementation I pasted above, I implemented a couple of main functions:
init(creatorFunction: @escaping CreatorFunction)
In the init
I'm just looking for a CreatorFunction
which at the moment of be used, you can do it this way:
func getNumber() -> Promise<Int> {
return .init { (observer) in
observer(.success(5))
}
}
What is that CreatorFunction
? It's the heart of a Promise
:
struct Promise<T> {
typealias ResultType = Result<T, Error>
typealias ResultObserver = (ResultType) -> Void
typealias CreatorFunction = (@escaping ResultObserver) -> Void
private let creatorFunction: CreatorFunction
init(creatorFunction: @escaping CreatorFunction) {
self.creatorFunction = creatorFunction
}
// ...
}
A Promise
is not much more than a struct that holds a function (let's call it the creator). That function is created taking another function (the observer) as an argument. The observer is a function that can be used to report the result of the creator. And that result is a Swift Result
type.
This takes some time to sink, but when it does, you have understood what a Promise
is and how to create one.
func then<E>(_ f: @escaping (T) -> E) -> Promise<E>
Does this look familiar to you? It's a map
. We are transforming a Promise<T>
to a Promise<E>
using a simple function (T) -> E
.
func then<E>(_ f: @escaping (T) -> E) -> Promise<E> {
return Promise<E> { observer in
self.creatorFunction { r in
observer(r.map(f))
}
}
}
This is very simple, just creating a new Promise
and "unbox" the result from the creatorFunction
.
func then<E>(_ f: @escaping (T) -> Promise<E>) -> Promise<E>
And this one? It's a flatMap
. We're converting a Promise<T>
to a Promise<E>
using a function that isn't that simple this time, because it returns a Promise<E>
and not a simple E
.
The implementation is as follows:
func then<E>(_ f: @escaping (T) -> Promise<E>) -> Promise<E> {
return Promise<E> { observer in
self.creatorFunction { firstResult in
switch firstResult {
case .success(let successResult):
f(successResult).creatorFunction { transformedResult in
observer(transformedResult)
}
case .failure(let error):
observer(.failure(error))
}
}
}
}
The key here is to use the creatorFunction
manually in the newly created Promise
.
static func all(_ promises: [Promise<T>], on queue: DispatchQueue = .main) -> Promise<[T]>
I'll let this one to you. It's basically a DispatchGroup
that notifies once all the Promises have finished resolving.
static func all(_ promises: [Promise<T>], on queue: DispatchQueue = .main) -> Promise<[T]> {
return Promise<[T]> { observer in
let group = DispatchGroup()
var values = [T]()
var hasFailed = false
for promise in promises {
group.enter()
promise.then { r in
switch r {
case let .success(value):
values.append(value)
case let .failure(error):
observer(.failure(error))
hasFailed = true
}
group.leave()
}
}
group.notify(queue: queue) {
guard !hasFailed else {
return
}
observer(.success(values))
}
}
}
@discardableResult func then(_ f: @escaping (ResultType) -> Void) -> Self
And, finally, the @discardableResult
function then
that basically lets us use the Promise
@discardableResult func then(_ f: @escaping (ResultType) -> Void) -> Self {
creatorFunction { r in f(r) }
return self
}
To sum up
This has been a very short and concise tutorial on how to implement a Promise. I'd like to encourage you to open a playground and try to implement async mechanisms like Promise, or event streams like Observable<E>
or Combine publishers. This could be a greatly rewarding exercise.
Posted on March 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.