Swiftpack.co - Package - glessard/deferred

Deferred Build Status

A lock-free, asynchronous Result for Swift 5 and up.

Deferred<T> allows you to chain computations together. A Deferred starts with an indeterminate, unresolved value. At some later time it may become resolved. Its value is then immutable for as long as that particular Deferred instance exists. Until a Deferred becomes resolved, computations that depend on it can be saved for future execution using a lock-free, thread-safe algorithm. The results of these computations are also represented by Deferred instances. Errors thrown at any point in a Deferred context are propagated effortlessly.

Deferred started out as an approximation of OCaml's module Deferred.

let d = Deferred<Double, Never> { // holds a `Double`, cannot hold an `Error`
  usleep(50000) // or a long calculation
  return 1.0
}
print(d.value!) // 1.0, after 50 milliseconds

A Deferred can schedule blocks of code that depend on its result for future execution using the notify, map, and flatMap methods, among others. Any number of such blocks can depend on the result of a Deferred. When an Error is thrown, it is propagated to all Deferreds that depend on it. A recover step can be inserted in a chain of transforms in order to handle potential errors.

let transform = Deferred<(Int) -> Double, Never> { i throws in Double(7*i) }
let operand = Deferred<Int, Error>(value: 6).delay(seconds: 0.1)
let result = operand.apply(transform: transform)                        // Deferred<Double, Error>
print(result.value!)                                                    // 42.0

The result property of Deferred (and its adjuncts, value , error and get()) will block the current thread until its Deferred becomes resolved. The rest of Deferred is lock-free.

Deferred can run its closure on a specified DispatchQueue, or at the requested DispatchQoS. The notify, map, flatMap, apply and recover methods also have these options. By default, closures will be scheduled on a queue created at the current quality-of-service (qos) class.

Task execution scheduling

Tasks execute when a request exists. When creating a Deferred object, allocations are made, but no code is run immediately. When a code that depends on a Deferred requests the result, then the task is scheduled for execution. Requests are made by calling the notify(), onValue(), onError(), or beginExecution() methods (non-blocking); the result, value or error properties (blocking). Requests propagate up a chain of Deferreds.

Long computations, cancellations and timeouts

Long background computations that support cancellation and timeout can be implemented easily by using the callback-style initializer for Deferred<T>. It takes a closure whose parameter is a Resolver. Resolver is associated with a specific instance of Deferred, and allows your code to be the data source of that Deferred instance. It also allows the data source to monitor the state of the Deferred.

func bigComputation() -> Deferred<Double, Never>
{
  return Deferred {
    resolver in
    DispatchQueue.global(qos: .utility).async {
      var progress = 0
      repeat {
        // first check that a result is still needed
        guard resolver.needsResolution else { return }
        // then work towards a partial computation
        Thread.sleep(until: Date() + 0.001)
        print(".", terminator: "")
        progress += 1
      } while progress < 20
      // we have an answer!
      resolver.resolve(value: .pi)
    }
  }
}

// let the child `Deferred` keep a reference to our big computation
let timeout = 0.1
let validated = bigComputation().validate(predicate: { $0 > 3.14159 && $0 < 3.14160 })
                                .timeout(seconds: timeout, reason: String(timeout))

do {
  print(validated.state)       // still waiting: no request yet
  let pi = try validated.get() // make the request and wait for value
  print(" ", pi)
}
catch Cancellation.timedOut(let message) {
  print()
  assert(message == String(timeout))
}

In the above example, our computation closure works hard to compute the ratio of a circle's circumference to its diameter, then performs a rough validation of the output by comparing it with a known approximation. Note that the Deferred object performing the primary computation is not retained directly by this user code. When the timeout is triggered, the computation is correctly abandoned and the object is deallocated. General cancellation can be performed in a similar manner.

Deferred is carefully written to support cancellation by leveraging the Swift runtime's reference counting. In every case, when a new Deferred is returned by a function from the package, that returned reference is the only strong reference in existence. This allows cancellation to work properly for entire chains of computations, even if it is applied to the final link of the chain, regardless of error types.

Using Deferred in a project

With the swift package manager, add the following to your package manifest's dependencies:

.package(url: "https://github.com/glessard/deferred.git", from: "6.4.0")

To integrate in an Xcode 10 project, tools such as Accio and xspm are good options. The repository contains an Xcode 10 project with manually-assembled example iOS target. It requires some git submodules to be loaded using the command git submodule update --init.

License

This library is released under the MIT License. See LICENSE for details.

Github

link
Stars: 3

Dependencies

Releases

API improvements - 2020-02-20 00:03:55

  • API improvements when the Failure type is Never: some accessors become non-Optional, while some functions become non-throwing.

  • Made the cancel() method available only when it is usable by repeating it in conditional extensions.

  • Added a customization point for cancellation for subclasses of Deferred.

  • Added versions of recover() for error types more specific than Error.

Typed Errors - 2020-02-19 23:57:09

Major update to deferred, adding typed errors as a generic parameter, using the same parameter names as the standard library's Result.

Another major change involves execution scheduling: execution now starts only once a Deferred's result has been explicitly requested in some way. The notify method is the primary way to accomplish this (along with onValue and onError), as well an explicit call to beginExecution().

Finally, the URLSessionTask wrapper has been moved to its own package, DeferredHTTP.

- 2019-05-23 00:09:28

Encapsulation improvements and improved lock-freedom.

- 2019-05-19 23:40:34

- 2019-05-16 00:21:20

4.4.0 - 2019-02-09 02:06:56

Improves performance by consolidating notifications and deallocations

  • fewer calls to dispatch_async
  • more work done in each dispatch_async

Improvements to and full testing of DeferredURLSessionTask.

Added a split method for any Deferred whose value type is a tuple of 2, 3 or 4 elements. The return value is a tuple of Deferred; if the original Deferred's outcome is an Error, then every Deferred in the split is determined with the same Error.

more api tweaks - 2018-10-20 12:33:06

  • added parameter labels to determine() methods (with fix-its.) The parameter-less versions didn't save anything much, while causing grief when Value has a type that implements Error

  • improved behaviour of firstValue(). Its outcome is now an error only when all the inputs are errors.

  • the old behaviour is still provided by firstDetermined().

api tweaks - 2018-09-24 16:49:56

made class Transferred: Deferred public as a convenience. renamed Determined<T> to Outcome<T> (no changes necessary.) renamed some parameter labels for clarity (with deprecation/rename warnings.)

Swift 4.2 update - 2018-09-01 01:32:25

Swift 4.1 Update - 2018-05-08 06:44:49

- 2017-10-11 04:27:11

- 2017-08-02 22:10:02

- 2017-01-30 23:45:31