-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
Replace start/observe closure syntax with distinct functions #2318
Conversation
Since Box is not needed anymore in Swift 2, we can encourage the use of Event<T, E> in start/observe calls to handle multiple cases. In cases where only one case is needed, startNext/startCompleted/ startError/startInterrupted replaces start(next:_:completed:_:error:_:interrupted:_) on SignalProducer, and observeNext/observeCompleted/observeInterrupted replaces observe(next:_:completed:_:error:_:interrupted:_) on Signal.
You could also make the case that |
subscriber.sendError(error as NSError) | ||
case .Completed: | ||
subscriber.sendCompleted() | ||
default: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this PR for this line right here: users will be aware of their need to explicitly ignore events, which wasn't the case before due to the default arguments!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is definitely not as compact on the caller side, but its probably worth it to be more explicit.
Interesting idea. If you only provide a And for the sake of comparison, the different ways for dealing with 2 event cases: // startNext:completed:
producer.startNext(next: { value in
print("value: \(value)")
}, completed: { _ in
print("completed")
})
// start switch
producer.start {
switch $0 {
case let .Next(value):
print("value: \(value)")
case let .Completed:
print("completed")
default:
break
}
}
// start if case
producer.start { event in
if case let .Next(value) = event {
print("value: \(value)")
} else if case .Completed = event {
print("completed")
}
} |
}) | ||
let selfDisposable = producer.start { event in | ||
switch event { | ||
case .Next(let value): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think most of the RAC code does case let .Next(value):
instead of moving the let
inside the case name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I'll change it.
Yes, the trailing closure works. It would make it basically identical to the previous closure based |
I tend to agree after looking at the different approaches side by side. My proposal would be to have 3 convenience methods (don't think interrupted is needed). startWithNext(next: T -> (), error: (E -> ())? = nil, completed: (() -> ())? = nil, interrupted: (() -> ())? = nil)
startWithError(error: E -> (), completed: (() -> ())? = nil, interrupted: (() -> ())? = nil)
startWithCompleted(completed: () -> (), error: (E -> ())? = nil, interrupted: (() -> ())? = nil)
// observeNext, etc. too The
Nope, since you can still use |
I agree that API would make the most sense 👍 |
@@ -316,8 +316,28 @@ extension SignalProducerType { | |||
/// | |||
/// Returns a Disposable which can be used to interrupt the work associated | |||
/// with the Signal, and prevent any future callbacks from being invoked. | |||
public func start(error error: (E -> ())? = nil, completed: (() -> ())? = nil, interrupted: (() -> ())? = nil, next: (T -> ())? = nil) -> Disposable { | |||
return start(Event.sink(next: next, error: error, completed: completed, interrupted: interrupted)) | |||
public func startWithNext(error error: (E -> ())? = nil, completed: (() -> ())? = nil, interrupted: (() -> ())? = nil, next: (T -> ())? = nil) -> Disposable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't realize that the next
param was going to be optional and have to be last. This is what I meant if the trailing closure syntax would still work in the case where it was first but the only non-optional param. If that's not possible, I don't think this is the right change and is potentially more confusing. At this point it may be back to the individual startWithNext
, startWithError
, etc. approach and no optional params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although, I would sacrifice the trailing closure syntax to require the associated closure be the first param and while still supporting the remaining optional closures
// this still seems reasonable to me ...
producer.startWithNext({
println("Next \($0)")
})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. The next-closure doesn't have to be optional but then the ordering will matter (e.g. completed
must precede next
), so that is not an option IMO. It does have to be the last argument to support the trailing closure.
I'm not sure. I think keeping it clean at the call site could be worth an somewhat un-intuitive argument ordering. This was the same style used with the previous overloading of start
, but I agree it gets more confusing with the new startWithNext
naming.
Looks like we have the following options:
producer.startWithNext({
println("Next \($0)")
})
producer.startWithNext({
println("Next \($0)")
},
error: {
println("Error \($0)")
})
producer.startWithNext {
println("Next \($0)")
}
producer.startWithNext(
next: {
println("Next \($0)")
},
error: {
println("Error \($0)")
})
producer.startWithNext {
println("Next \($0)")
}
producer.startWithError {
println("Error \($0)")
}
I like (2) the best, but would also be ok with (1). The problem with (1) is that it looks like it should support trailing closure syntax, but doesn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. I think keeping it clean at the call site could be worth an somewhat un-intuitive argument ordering.
This is really why I don't like (2) because it doesn't work when you have more than one closure argument. I think I'm back to the (3) camp with single closure argument and use start
if you want to handle multiple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After thinking about it a bit more, I tend to agree it might be the least confusing option. Time will tell if it would be too cumbersome to use.
So if we agree on this, I'll go ahead and revert my last commit and go back to the original suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@neilpa that should be it. Also changed the "case let" style to be consistent across the code base. |
Awesome! Thanks for putting all the time/discussion into this one. |
Replace start/observe closure syntax with distinct functions
I didn't reallse see this convo but I'd like to say the new API in my opinion is inferior to the old API because there is no way to chain the subscriptions and now if we ever have to listen in on more than one event type we have to run a ugly as switch statement. I'd much much much prefer to have the optional closures back. |
Actually maybe it's not too bad having the switch statement… guess it depends on how much logic has to go into these blocks and suppose it should probably be minimal. |
Issues #2311 contains the background discussion for this change.
In Swift 2 (and the swift2 branch of this repo), the
SinkOf<T>
type is gone and instead a normal closureT -> ()
is used. This is great but it introduces a potential risk due to the behaviour change when callingSignalProducer.start()
with a trailing closure. In Swift 1.x, the trailing closure would default to the next argument, and in order to provide a sink you would need to pass in aSinkOf<Event<T>>
. In Swift 2 however, the trailing closure will match the new sink typeT -> ()
. From the call site, the call looks identical after migrating to the swift2 branch but the behaviour is different. This could potentially introduce issues in cases where the argument to start is ignored(_)
, as the type checker will not catch it and the closure will be called on next/interrupted/error/completed and not just on next.Since Box is no longer needed with Swift 2, this PR replaces the
start(next:_:completed:_:error:_:interrupted)
syntax with only onestart
andobserve
that takes a sink when handling multiple cases, as well as separate functionsstartNext()
,startCompleted()
,startError()
,startInterrupted()
for handling a single case.