startWith*
was a compromise made in ReactiveCocoa 4.0 as a means to provide disambiguated, trailing closure compatible shorthands to start
. While they have been serving the community well since then, the community of Swift has established a new set of API guidelines for Swift. It specifically asks for API designs to look as grammatical as possible at the call site.
startWith*
is probably the APIs with the largest impact in ReativeSwift. Unfortunately, it is grammatically incorrect in its use of preposition IMO. For example, let’s say we have a line producer.startWithNext { value in action(value) }
. At the call site, it is generally read as:
Start [a producer] with Next, and pipe the value into the action.
The problem here is that we do not start a producer with next events, values or errors, but for the next events, the values and the errors. We start it for receiving the values, for any potential errors, for being notified its completion, etc.
I propose:
- replacing
startWith*
withstartFor*
, and specificallystartWithNext
withstartForValues
; - adding a label
invoking
to the first argument for allobserve*
andstart[For*]
methods; and - renaming
Event.next
toEvent.nextValue
, and noun-ify otherEvent
enum cases.
This proposal was not intended to touch on the grand scheme of mutating vs non-mutating naming of operators.
extension SignalProducerProtocol {
public func start(with observer: Observer<Value, Error>) -> Disposable
public func start(invoking action: (Event<Value, Error>) -> Void) -> Disposable
public func startForValues(invoking action: (Value) -> Void) -> Disposable
public func startForResults(invoking action: (Result<Value, Error>) -> Void) -> Disposable
public func startForError(invoking action: (Error) -> Void) -> Disposable
public func startForCompletion(invoking action: () -> Void) -> Disposable
public func startForInterruption(invoking action: () -> Void) -> Disposable
public func startForTermination(invoking action: () -> Void) -> Disposable
}
extension SignalProtocol {
public func observe(with observer: Observer<Value, Error>) -> Disposable?
public func observe(invoking action: (Event<Value, Error>) -> Void) -> Disposable?
public func observeValues(invoking action: (Value) -> Void) -> Disposable?
public func observeResults(invoking action: (Result<Value, Error>) -> Void) -> Disposable?
public func observeError(invoking action: (Error) -> Void) -> Disposable?
public func observeCompletion(invoking action: () -> Void) -> Disposable?
public func observeInterruption(invoking action: () -> Void) -> Disposable?
public func observeTermination(invoking action: () -> Void) -> Disposable?
}
A more verbose alternative is startWith*Observer
, i.e.
extension SignalProducerProtocol {
public func startWithValueObserver(_ body: (Value) -> Void) -> Disposable
}
/// It makes sense for sure. :-)
producer.startWithValueObserver { print($0) }
Since start
has been established as a term of art for SignalProducer
since RAC 3.0, changes should be avoided if possible. But there are also a few ways I have thought of:
- Renames
start[With*]
toobserve*
. - Brings back
subscribe
as a replacement tostart
, and invents a new meaning contrasting withobserve
. - A new wrapper type
LazySignal
that can be created fromSignalProducer
, exposing only a set ofobserve*
methods likeSignal
. It is like (2), but requires an explicit call to "produce" a lazy signal. - Make
Observer
an enum. However, the type system cannot infer the type for dot syntax if trailing closures are used.
Some might argue why the argument labels are not preferred. While the primary reason is of course losing the unambiguity for trailing closures to work, let’s consider the full signature:
func start(forValues action: (Value) -> Void) -> Disposable
func start(values action: (Value) -> Void) -> Disposable
func start(next action: (Value) -> Void) -> Disposable
When one saw a forValues
label or a values
label at the call site, one might infer the expected arguments are some values. But then… oops - a red mark 🔴 shows, telling instead that a closure is expected. Then a question might arise - is this closure a generator or a receiver of values?
As for having a next
label, it is ambigious if you read it literally:
Start [the producer] with the next action being the closure.
Clarity at the point of use.
Clarity is more important than brevity.
Prefer method and function names that make use sites form grammatical English phrases.
When the first argument forms part of a prepositional phrase, give it an argument label. Otherwise, if the first argument forms part of a grammatical phrase, omit its label, appending any preceding words to the base name, e.g. x.addSubview(y)
The names of other types, properties, variables, and constants should read as nouns.