Skip to main content
2021-2024 Swift GCD NSLock PovioKit

PromiseKit

Production Promise library built from algebraic first principles, shipped to ~50K DAU across multiple iOS apps

Overview

PromiseKit is a production Promise/Future library embedded in PovioKit -- a modular Swift Package distributed as 7 independent libraries. It has zero external dependencies and serves as the async backbone: the networking module imports it to expose a promise-based API for all HTTP operations. The library shipped to ~50K daily active users across multiple iOS apps including Dropbox Stacks, evolving over 3+ years through PRs addressing race conditions, synchronization primitives, and concurrency requirements.

Architecture

PromiseKit architecture

The design separates read-only observation (Future<Value, Error>) from write capability (Promise<Value>). Future holds the lock, observer list, and result storage as the synchronization core; Promise inherits and adds the public resolve/reject API. Combinators form a proper algebraic hierarchy where complex operations derive from simpler ones -- map from flatMap, reduce from fold -- with only flatMap and flatMapError as fundamental operations.

Key Concepts

Algebraic Combinator Derivation

Algebraic Combinator Derivation

The combinators form a proper algebraic hierarchy: map derives from flatMap (map = flatMap . pure from Haskell), mapError from flatMapError, fold from and + flatMap, reduce from fold. There are really only two fundamental operations: flatMap and flatMapError. Everything else composes from those two.

Haskell Operator Precedence Groups

Custom Swift precedence groups mirror Haskell's typeclass tower: SequencePrecedence (applicative) > ChoicePrecedence (alternative) > FlatMapPrecedence (monadic bind). This means expressions like `Promise.error(.e) <|> Promise.value(10) >>- { ... }` parse with correct algebraic semantics.

Code Highlights

flatMap — the monadic bind (everything derives from this)
func flatMap<U>(
  on dispatchQueue: DispatchQueue? = .main,
  with transform: @escaping (Value) throws -> Promise<U>
) -> Promise<U> {
  Promise<U> { seal in
    self.finally {
      switch $0 {
      case .success(let value):
        dispatchQueue.async {
          do {
            try transform(value).finally {
              switch $0 {
              case .success(let value):
                seal.resolve(with: value, on: dispatchQueue)
              case .failure(let error):
                seal.reject(with: error, on: dispatchQueue)
              }
            }
          } catch { seal.reject(with: error, on: dispatchQueue) }
        }
      case .failure(let error):
        seal.reject(with: error, on: dispatchQueue)
      }
    }
  }
}

// map is derived: map f = flatMap (pure . f)
func map<U>(on dq: DispatchQueue? = .main,
            with transform: @escaping (Value) throws -> U) -> Promise<U> {
  flatMap(on: dq) { do { return .value(try transform($0)) }
                     catch { return .error(error) } }
}
concurrentlyDispatch — bounded work pool with per-slot retry
public func concurrentlyDispatch<T>(
  spawnTask next: @escaping (Int) -> Promise<T>?,
  concurrent: Int, retryCount: Int = 2,
  on dispatchQueue: DispatchQueue? = .main
) -> Promise<()> {
  .init { seal in
    let barrier = DispatchQueue(label: "barrier", attributes: .concurrent)
    var segmentIndex = concurrent
    var active = [(promise: Promise<T>, retryCount: Int, segmentIndex: Int)]()

    func observer(_ result: Result<T, Error>, arrayIndex: Int) {
      barrier.async(flags: .barrier) {
        switch result {
        case .success:
          guard let promise = next(segmentIndex) else {
            if active.allSatisfy({ $0.promise.isFulfilled }) {
              seal.resolve(on: dispatchQueue) }; return }
          promise.finally { observer($0, arrayIndex: arrayIndex) }
          active[arrayIndex] = (promise, 0, segmentIndex)
          segmentIndex += 1
        case .failure where active[arrayIndex].retryCount < retryCount:
          let idx = active[arrayIndex].segmentIndex
          let promise = next(idx)!
          promise.finally { observer($0, arrayIndex: arrayIndex) }
          active[arrayIndex] = (promise, active[arrayIndex].retryCount + 1, idx)
        case .failure(let error):
          active.forEach { $0.promise.reject(with: error) }
          seal.reject(with: error, on: dispatchQueue)
        }
      }
    }
    // spawn initial pool
    for i in 0..<concurrent {
      guard let p = next(i) else { break }
      active.append((p, 0, i))
    }
    active.enumerated().forEach { (i, t) in t.promise.finally { observer($0, arrayIndex: i) } }
  }
}
Haskell operators — >>- <|> <*> <^> with algebraic precedence
// Applicative > Alternative > Monad (matches Haskell's typeclass tower)
precedencegroup SequencePrecedence { higherThan: ChoicePrecedence }
precedencegroup ChoicePrecedence   { higherThan: FlatMapPrecedence }
precedencegroup FlatMapPrecedence  { higherThan: AssignmentPrecedence }

// >>- monadic bind (flatMap infix)
func >>-<T, U>(p: Promise<T>, f: @escaping (T) throws -> Promise<U>) -> Promise<U> {
  p.flatMap(on: .main, with: f)
}
// <|> alternative (try left, on failure try right)
func <|><T>(left: Promise<T>, right: @autoclosure @escaping () -> Promise<T>) -> Promise<T> {
  left.flatMapError(on: .main) { _ in right() }
}
// <*> applicative apply
func <*><T, U>(left: Promise<(U) -> T>, right: Promise<U>) -> Promise<T> {
  right >>- { r in left.map(on: .main) { $0(r) } }
}

// Usage: curry(makeUser) <^> fetchName() <*> fetchEmail() <*> fetchAge()

Highlights

  • Designed and built a production Promise library from algebraic first principles -- map derives from flatMap, reduce from fold, operators match Haskell's typeclass precedence hierarchy
  • Evolved thread safety through production-driven iterations -- migrated to NSLock + barrier dispatch, immediately fixing a race condition exposed by the migration
  • Built domain-specific concurrency primitives: bounded work pool with per-slot retry tracking, server-driven dynamic polling, and zero-overhead serial execution
  • Implemented genuine Haskell operators in production Swift -- applicative apply, alternative, monadic bind, functor map, plus curry functions up to 6-arity
  • Zero-dependency library serving as the async foundation for PovioKit's networking layer across all Povio Labs apps