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

apply operator #73

Closed
acchou opened this issue Jan 25, 2017 · 4 comments
Closed

apply operator #73

acchou opened this issue Jan 25, 2017 · 4 comments

Comments

@acchou
Copy link
Contributor

acchou commented Jan 25, 2017

Name and description

I'd like to propose a very simple operator, apply, which takes a transformation function that takes an Observable and returns an Observable, and simply runs it and returns its value:

extension ObservableType {
    func apply<T>(_ transform: (Observable<Self.E>) -> Observable<T>) -> Observable<T> {
        return transform(self.asObservable())
    }
}

Motivation for inclusion

It's idiomatic to write new operators as extensions of ObservableType. This makes sense for many custom operators because it preserves the chaining syntax of RxSwift. But sometimes this style of extension operator is uncomfortable when the operator is not generic in nature, but might perform some combination of application-specific actions, and may even have side-effects.

In these cases it makes sense to write a simple function to add operations to an Observable, for example:

// Take an ordinary Rx-style request and add retry, application-specific side-effect, and error parsing.
func requestPolicy(_ request: Observable<Void>) -> Observable<Response> {
    return request.retry(maxAttempts)
        .do(onNext: sideEffect)
        .map { Response.success }
        .catchError { error in Observable.just(parseRequestError(error: error)) }

This function can then be applied to several requests to apply consistent retries, side-effects, and error handling:

let request1 = Observable<Void>.create { ... }
let request2 = Observable<Void>.create { ... } 
let request1Resilient = requestPolicy(request1)
let request2Resilient = requestPolicy(request2)

But this syntax is awkward because it requires each policy to be wrapped around the observable. The use of apply returns this to a more familiar chaining syntax:

let request1Resilient = request1.apply(requestPolicy)
let request2Resilient = request2.apply(requestPolicy)

This is especially useful when composing multiple transform functions:

// Without apply
let multiplePolicy = policy3(policy2(policy1(request)))
// With apply
let multiplePolicy = request
  .apply(policy1)
  .apply(policy2)
  .apply(policy3)

There may also be a desire to have a version with one or more arguments to apply:

    func apply<A, T>(arg: A, _ transform: (A, Observable<Self.E>) -> Observable<T>) -> Observable<T> {
        return transform(arg, self.asObservable())
    }

Example of use

See above.

@fpillet
Copy link
Member

fpillet commented Feb 8, 2017

Interesting proposal! So this operator is semantic sugar to better align in chain of operators ? I like it! Let's go ahead and add it.

@acchou
Copy link
Contributor Author

acchou commented Feb 8, 2017

Yes it's just syntactic sugar but I think it encourages code that factors out common operator chains into functions that can be reused and composed together. I've seen this when wrapping 3rd party APIs, sometimes there are many API calls that are similar in their error codes and can share error handling logic, retry logic, etc. I imagine there are other scenarios too.

I've recently also been looking at another common pattern that arises with withLatestFrom:

buttonPressed
  .withLatestFrom(inputObservable)
  .flatMap { input in createRequest(input).catchError { error in handle(error) } }

This is interesting because it is basically setting up a function call (the createRequest) in an inverted fashion, by first creating the input stream. Using apply we could factor out this logic in case it can be reused for other requests and buttons... so it helps but doesn't change the awkward style here. Maybe it would be more intuitive for some people to write:

buttonPressed
  .call(createRequest, inputObservable) { error in handle(error) }

I guess it's a separate idea from apply, so maybe I'll submit a separate issue for it :)

@freak4pc
Copy link
Member

I like this a lot !

@fpillet fpillet closed this as completed Feb 25, 2017
@DivineDominion
Copy link

@acchou the call syntax looks interesting! If the arguments had meaningful labels, I'd love to change my withLatestFroms :)

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

No branches or pull requests

4 participants