Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promise draft #664

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
182 changes: 182 additions & 0 deletions ReactiveSwift.playground/Pages/Sandbox.xcplaygroundpage/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,186 @@ import Foundation
A place where you can build your sand castles 🏖.
*/

enum PromiseResult<Value, Error: Swift.Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just use Result for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well that was my intention in the first place, but then I couldn't work around the case with nil error.
That's actually a questionable decision to store it this way, other implementations chose to wrap a user error into additional error enum like

enum PromiseError: Error {
     case nested(error)
     case interrupted
}

In this case standard Result would do. I'm open for suggestions here - including an option to fatalError the case of interrupted / empty source producer. Which is of course not something I'd agree for ;)

case fulfilled(Value)
case rejected(Error?) // nil error means that the producer was interrupted or empty

init(value: Value) {
self = .fulfilled(value)
}

init(error: Error?) {
self = .rejected(error)
}

var value: Value? {
switch self {
case .fulfilled(let value): return value
case .rejected(_): return nil
}
}

var error: Error? {
switch self {
case .fulfilled(_): return nil
case .rejected(let error): return error
}
}
}

protocol Thenable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we'd get any benefit from this protocol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah.. I just snatched it from PromiseKit, after reading the specs which refer to Thenable as something separate from the Promise itself. Technically, there could be thin auxiliary classes implementing the protocol directly, without the overhead of the full Promise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have refactored the code, and now Thenable is a type-erased ThenableType, which allows for a number of nice optimisations

associatedtype Value
associatedtype Error: Swift.Error

func await(notify: @escaping (PromiseResult<Value, Error>) -> Void) -> Disposable
}

extension Thenable {

@discardableResult
func chain<T: Thenable>(on: Scheduler = ImmediateScheduler(), body: @escaping (PromiseResult<Value, Error>) -> T) -> Promise<T.Value, Error> where T.Error == Error {
return Promise<T.Value, Error> { resolve in
return self.await { result in
on.schedule {
let thenable = body(result)
thenable.await(notify: resolve)
}
}
}
}

@discardableResult
func then<T: Thenable>(on: Scheduler = ImmediateScheduler(), body: @escaping (Value) -> T) -> Promise<T.Value, Error> where T.Error == Error {

return self.chain { (result) -> T in
guard let value = result.value else {
return Promise<T.Value, Error>(error: result.error) as! T
}
return body(value)
}

}
}

struct Promise<Value, Error: Swift.Error>: Thenable, SignalProducerConvertible {
typealias Result = PromiseResult<Value, Error>

enum State {
case pending
case resolved(Result)

var promiseResult: Result? {
switch self {
case .pending: return nil
case .resolved(let result): return result
}
}
}

private let state: Property<State>

private let (_lifetime, _lifetimeToken) = Lifetime.make()

var lifetime: Lifetime { return _lifetime }

var producer: SignalProducer<Value, Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be SignalProducer.init(_: Promise<Value, Error>) instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed. state can be made fileprivate and an extension for SignalProducer could be defined. Also it would be cleaner to make this a let property cause there's no reason in creating a new signal producer for every caller

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return SignalProducer { [weak state] observer, lifetime in
guard let state = state else {
observer.sendInterrupted()
return
}

lifetime += state.producer.startWithValues {
guard case .resolved(let result) = $0 else {
return
}
switch result {
case .fulfilled(let value):
observer.send(value: value)
observer.sendCompleted()
case .rejected(let error?):
observer.send(error: error)
case .rejected(nil):
observer.sendCompleted()
}
}
}
}

init(_ resolver: @escaping (@escaping (Result) -> Void) -> Disposable) {
let promiseResolver = SignalProducer<State, NoError> { observer, lifetime in
lifetime += resolver { result in
observer.send(value: .resolved(result))
observer.sendCompleted()
}
}
state = Property(initial: .pending, then: promiseResolver)
}

init(_ resolver: @escaping (@escaping (Value) -> Void, @escaping (Error) -> Void) -> Disposable) {
self.init { resolve in
return resolver(
{ resolve(.fulfilled($0)) },
{ resolve(.rejected($0)) }
)
}
}

init(value: Value) {
self.init(result: .fulfilled(value))
}

init(error: Error?) {
self.init(result: .rejected(error))
}

init(result: Result) {
state = Property(initial: .resolved(result), then: .empty)
}

init(producer: SignalProducer<Value, Error>) {
self.init { resolve in
return producer.start { event in
switch event {
case .value(let value):
resolve(.fulfilled(value))
case .failed(let error):
resolve(.rejected(error))
case .completed, .interrupted:
// this one will be ignored if real value/error was delivered first
resolve(.rejected(nil))
}
}
}
}

func await(notify: @escaping (PromiseResult<Value, Error>) -> Void) -> Disposable {
return state.producer
.filterMap { $0.promiseResult }
.startWithValues(notify)
}
}



extension SignalProducer {
func makePromise() -> Promise<Value, Error> {
return Promise<Value, Error>(producer: self)
}
}

func add28(to term: Int) -> Promise<Int, NoError> {
return Promise { fulfil, _ in
fulfil(term + 28)
return AnyDisposable()
}
}

let answer = SignalProducer<Int, NoError>(value: 14)
.makePromise()
.then { add28(to: $0) }

answer.await { (result) in
print(result)
}

2 changes: 1 addition & 1 deletion ReactiveSwift.playground/contents.xcplayground
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='6.0' target-platform='macos' display-mode='rendered'>
<playground version='6.0' target-platform='macos' display-mode='rendered' executeOnSourceChanges='false'>
<pages>
<page name='Sandbox'/>
<page name='SignalProducer'/>
Expand Down