Skip to content

Instantly share code, notes, and snippets.

@andersio
Created July 4, 2016 07:11
Show Gist options
  • Save andersio/af1a51529b1c0b11811ca108acfd041f to your computer and use it in GitHub Desktop.
Save andersio/af1a51529b1c0b11811ca108acfd041f to your computer and use it in GitHub Desktop.

Rationale

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.

Design

I propose:

  1. replacing startWith* with startFor*, and specifically startWithNext with startForValues;
  2. adding a label invoking to the first argument for all observe* and start[For*] methods; and
  3. renaming Event.next to Event.nextValue, and noun-ify other Event enum cases.
Out of scope

This proposal was not intended to touch on the grand scheme of mutating vs non-mutating naming of operators.

A glimpse

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?
} 

Alternatives considered

Other possible solutions...

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:

  1. Renames start[With*] to observe*.
  2. Brings back subscribe as a replacement to start, and invents a new meaning contrasting with observe.
  3. A new wrapper type LazySignal that can be created from SignalProducer, exposing only a set of observe* methods like Signal. It is like (2), but requires an explicit call to "produce" a lazy signal.
  4. Make Observer an enum. However, the type system cannot infer the type for dot syntax if trailing closures are used.
About the first argument label...

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.

Relevant guidelines

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment