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 all commits
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
244 changes: 244 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,248 @@ 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 ThenableType {
associatedtype Value
associatedtype Error: Swift.Error

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

struct Thenable<Value, Error: Swift.Error>: ThenableType {
typealias AwaitFunction = (@escaping (PromiseResult<Value, Error>) -> Void) -> Disposable
typealias Result = PromiseResult<Value, Error>

private let awaitImpl: AwaitFunction

@inline(__always)
func await(notify: @escaping (Result) -> Void) -> Disposable {
return awaitImpl(notify)
}

init(_ impl: @escaping AwaitFunction) {
self.awaitImpl = impl
}

init<T: ThenableType>(_ thenable: T) where T.Value == Value, T.Error == Error {
self.awaitImpl = { notify in
return thenable.await(notify: notify)
}
}
}

extension Thenable {
init(_ result: Result) {
self.init { notify in
notify(result)
return AnyDisposable()
}
}
init(_ value: Value) {
self.init(.fulfilled(value))
}
init(_ error: Error?) {
self.init(.rejected(error))
}

static func never() -> Thenable<Value, Error> {
return Thenable { _ in AnyDisposable() }
}
}

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

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

return self.chain { result -> Thenable<T.Value, Error> in
guard case .fulfilled(let value) = result else {
return Thenable(result.error)
}
return Thenable(body(value))
}
}
}

struct Promise<Value, Error: Swift.Error>: ThenableType, 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(self)
}

fileprivate init(_ state: Property<State>) {
self.state = state
}

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

static var never: Promise<Value, Error> {
return Promise(Property(initial: .pending, then: .empty))
}
}

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

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

extension Promise {

init(_ result: Result) {
self.init(Property(initial: .resolved(result), then: .empty))
}

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

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

extension Promise {
init(_ producer: SignalProducer<Value, Error>) {
self.init { resolve, lifetime in
lifetime += 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))
}
}
}
}
}

extension SignalProducer {
init<T: ThenableType>(thenable: T) where T.Value == Value, T.Error == Error {
self.init { observer, lifetime in
lifetime += thenable.await { result in
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<T: ThenableType>(_ thenable: T) where T.Value == Value, T.Error == Error {
self.init(thenable: thenable)
}

init(_ promise: Promise<Value, Error>) {
self.init(thenable: promise)
}
}

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

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

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