DEV Community

loading...

MVP: Minimum Viable Promise

fmo91 profile image Fernando Martín Ortiz ・4 min read

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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

image

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))
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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))
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

@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
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

pic
Editor guide