TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Apr 2015 |
SPMSupports SPM | ✗ |
Maintained by Junior B..
DeLorean is a Functional Reactive Kit for Swift.
// Currently not supporting Swift 1.2
DeLorean is a framework (still WIP) focused in delivery a complete implementation of some fundamentals of asynchronous programming in a functional style for Swift. The framework wants to cover the following 2 big concepts:
Swift is gaining traction as Functional Programming language, generating a lot of debates. This framework has been created to implement two big concepts in a functional style: Parallel Non-blocking Operations (Futures/Promises) and Reactive Programming (Streams).
The reason why this two different worlds can work together is because Future and Promise are concepts that perfectly fit definite operations, like a HTTP request or a JSON deserialization. On the other hand, Reactive Programming fits perfectly to handle operations with random inputs, like UI interactions (f.e: TextField input), location manager updates, external accessories interactions, websockets, etc…
You can freely contribute to the development of DeLorean, please checkout the repository, add the features you would like to have and create a pull-request. This repository uses the GitFlow Workflow, please check it if you don’t know how it works before sending a pull-request. Pull-requests to master branch are going to be ignored (if not for an extremely urgent hotfix). Please consider to create a dedicated test for the feature, so future updates can avoid to break current-working code.
I strongly recommend you to read the following resources to understand what inspired this framework:
or check these online courses:
The framework is in development and the current status will be updated regularly. The current development status is:
Flux Capacitor has been completed and includes all major operations to deal with Promises and Futures like: flatMap, zip, then, fallbackTo, etc…
Mr. Fusion development is in early stage, please don’t use this module right now.
Support for Carthage and Cocoapods have not been tested or included, yet.
The concept is very simple, a Future is a sort of a placeholder object that you can create for a result that has to be currently computed. Futures provide a nice way to perform many operations in parallel, using an efficient and non-blocking way. By default, the result of the Future is computed concurrently and can be later collected. Composing tasks in this way tends to result in safer, asynchronous, non-blocking parallel code. While Futures are read-only placeholders, Promises are an extension of Futures that can be also written.
Note: This implementation is Scala-complaint
, so the Scala’s documentation about Future and Promises is actually also valid.
A Future is an object holding a result which may become available at some point. The result is usually the achieved with some other computation:
So basically, asking for Future’s computation result can have the following scenarios:
nil
, if the computation is not completed.Value(t)
, in case it contains a valid result and is completedError(e)
if it contains an errorOnce the result has been computed, all registered callbacks are fired.
The basic example with Future
is the download of a file, with parse and display.
future {
var response: NSURLResponse?
var error : NSError? = nil
if let data = NSURLConnection.sendSynchronousRequest(contentRequest, returningResponse: &response, error: &error) {
return Result(data)
}
return Result(error!)
}.onSuccess(){ (result: NSData) in
//Function parseDataAndUpdateUI just parse the data and basically displays the result
self.parseDataAndUpdateUI(result)
return
}.onFailure(){ (e: NSError) in
//Function displayError just displays the error
self.displayError(e)
return
}
A Future
can also be used to compute Void
blocks asynchronously:
futureVoid {
self.performLongTask() // defined as: func performLongTask() {...}
}
The Scala/Java implementation of Future
and Promise
takes advantage of the Try/Catch/Finally
statement, relying on the catch of Throwable
(this includes Exception
and Error
). In Swift we don’t have the Try/Catch
instruction and Apple has clearly expressed that it will not be included. This language design choice forced this implementation to require the Result
structure to be returned as value at the end of the computational block. The first example shows how is possible to pass the error returned by a failed request or the data as result of a successful one.
Futures are objects created by asynchronous computations started using the future method. Promises are an extension of futures and futures can be created using this new type.
Futures are defined as read-only placeholders, but sometimes we need to be able to write in those placeholders. Promises have been created to achieve this behavior. A promise can be fulfilled only once, that means that we can either success or fail to fulfill a promise a single time, any further call to success
or failure
will be ignored.
Consider the following example, in which a value is produced by a computation and is handed off to another computation which requires that value to be consumed. This process to pass a value is achieved using a Promise
.
let stringURL = "http://rate-exchange.appspot.com/currency?from=USD&to=EUR"
var response: NSURLResponse?
var error : NSError? = nil
let contentRequest = NSURLRequest(URL: NSURL(string: stringURL)!)
let p = Promise<NSData>()
let f = p.future()
futureVoid {
if let data = NSURLConnection.sendSynchronousRequest(contentRequest, returningResponse: &response, error: &error) {
p.success(data)
} else {
p.failure(error!)
}
}
f.onFailure(){ (e: NSError) in
//Function displayError just displays the error
self.displayError(e)
}
f.onSuccess{ data in
//Process the data and eventually display it
self.displayData(data)
}
The library comes with many way to combine futures, here are some examples:
With the then
function we can chain futures:
// Example response:
// {"to": "CHF", "rate": 0.96462400000000004, "from": "USD"}
let stringURL = "http://rate-exchange.appspot.com/currency?from=USD&to=CHF"
future {
var response: NSURLResponse?
var error : NSError? = nil
let contentRequest = NSURLRequest(URL: NSURL(string: stringURL)!)
if let data = NSURLConnection.sendSynchronousRequest(contentRequest, returningResponse: &response, error: &error) {
return Result(data)
}
return Result(error!)
}.then { (data: NSData) in
var error : NSError? = nil
if let dict = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &error) as! Dictionary<String, AnyObject>? {
return Result(dict)
}
return Result(error!)
}.onSuccess(){ (result: Dictionary<String, AnyObject>) in
self.processData(result)
}.onFailure(){ error in
self.handleError(e)
}
Fmap
creates a new Future
by applying a function to the successful result of this future, returning a new future as the result of the function. If this future is completed with an error, the new one will contain the same error.
let f = future {
return Result(fibonacci(35)) //returns 9'227'465
}.fmap{ value in
return future {
return Result(value + fibonacci(35)) //returns 9'227'465
}
}
f.onSuccess() { value in
// value is 18'454'930 (9'227'465 * 2)
}
Zip
function zips the values of this
and that
future, and creates a new future holding the tuple of their results.
If this
future fails, the resulting future is failed with the same error of this
. Otherwise, if that
future fails, the resulting future is failed with the error stored in that
.
WARNING! The current implementation of Swift, doesn’t work with a plain generics tuples. To workaround this, we are using an array to box the current result tuple. As soon as Swift will correctly handle generics and tuples, we are going to change this method with a better implementation.
// Example response:
// {"to": "CHF", "rate": 0.96462400000000004, "from": "USD"}
let usdToChfURL = "http://rate-exchange.appspot.com/currency?from=USD&to=CHF"
let chfToEurURL = "http://rate-exchange.appspot.com/currency?from=CHF&to=EUR"
let usdToChf : Future<NSData> = future {
var response: NSURLResponse?
var error : NSError? = nil
let contentRequest = NSURLRequest(URL: NSURL(string: usdToChfURL)!)
if let data = NSURLConnection.sendSynchronousRequest(contentRequest, returningResponse: &response, error: &error) {
return Result<NSData>(data)
}
return Result(error!)
}
let chfToEur : Future<NSData> = future {
var response: NSURLResponse?
var error : NSError? = nil
let contentRequest = NSURLRequest(URL: NSURL(string: chfToEurURL)!)
if let data = NSURLConnection.sendSynchronousRequest(contentRequest, returningResponse: &response, error: &error) {
return Result<NSData>(data)
}
return Result(error!)
}
let zipped = usdToChf.zip(that: chfToEur)
zipped.onSuccess { value in
let usdToChfData = value[0].0
let chfToEurData = value[0].1
// Process both values
}
Please consider to read the in-code documentation for map
, filter
, recover
, recoverWith
and fallbackTo
.
Mr. Fusion is still in a very early development stage. This module is the Reactive Core of DeLorean, if you are looking for an implementation similar to what Rx extensions are providing, you have probably found it.
Note: The decision to not keep the names like Observer, Observable, etc… has been made after a couple of hours of toughts. I don’t believe they fit the Cocoa world so well. Feedbacks about it are highly appreciated.
An Emitter
is an entity that is emitting events. An event can be of any kind, from a simple Int
to a complex object, passing through UI events. An emitter can be, for example, used to keep track of the user location, producing coordinates of the current position or it can be used to react on inputs by user on a button, slider or textfield.
To create an Emitter
of type Int
you can just use the simple init:
let emitter = Emitter<Int>()
You can then emit values, complete the emitter or make it fail.
emitter.emit(5)
// Error requires an instance of NSError
emitter.error(e)
emitter.complete()
Emitters are composable, it means we can merge, zip, concatenate, etc… two or more emitters, generating new emitters.
A Subscription
is a generalized mechanism to process push-based notification. In other resources it is also known as the observer. A Subscription
is an object that represents a processor of notifications or events (also know as consumer).
To subscribe, the best way is to call the subscribe
method on an Emitter
.
let subscription = Subscription<Int>()
emitter.subscribe(subscription)
Another way is to request an empty subscription from an Emitter
let subscription = emitter.subscription()
In iOS, subclass of UIControl
have a default Emitter
object that fires objects of type ControlEvent
. A ControlEvent
requires a key, as instance of UIControlEvents
and a value, that can be an Int
, a Double
, a String
, a Float
or a NSDate
.
Here an example using a text field that fires a search only with strings longer than 3 characters:
// e is an instance of UIControlEvents<String>
searchTextField?.emitter.filter({ e in
return count(e.value) > 3
}).subscription().onNext() { event in
self.search(term: event.value)
}
We can also use the same process to filter over UIButtons
:
searchBtn.emitter.filter({e in return e.key == .TouchUpInside}).subscription().onNext(){ _ in
self.search(term: searchTextField?.text)
}
A Scheduler
provides a set of methods to access commonly used Execution Contexts and acts as a base class for all schedulers.
Predefined schedulers are:
/// The scheduler where all UI events are performed
public static let UIScheduler = Scheduler(ExecutionContext.mainContext)
/// The default scheduler
public static let Default = Scheduler()
/// The background scheduler for very time consuming producers
public static let BackgroundScheduler = Scheduler(ExecutionContext.concurrentContext)
Note: UI Scheduler schedules on the main queue.
Implementations of Result.swift
and Operators.swift
are originally made by Maxwell Swadling and are part of the Swiftz functional library.
The MIT License (MIT)
Copyright © 2015 Junior B. (bonto.ch)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software
), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS
, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.