RxSwiftCommunity / RxSwiftExt

A collection of Rx operators & tools not found in the core RxSwift distribution

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

apply operator

acchou opened this issue · comments

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.

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.

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 :)

I like this a lot !

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