RxSwift: Cannot flatMap a Single trait to a Completable trait

⚠️ If you don’t have something to report in the following format, it will probably be easier and faster to ask in the slack channel first. ⚠️

⚠️ Please take you time to fill in the fields below. If we aren’t provided with this basic information about your issue we probably won’t be able to help you and there won’t be much we can do except to close the issue 😦 ⚠️

If you still want to report issue, please delete above statements before submitting an issue.

Short description of the issue:

Cannot flatMap a Single trait to a Completable trait

Expected outcome:

I expect that the flatMap would let me convert to a Completable trait like in the example below.

What actually happens:

I get the following error:

 'flatMap' produces 'PrimitiveSequence<SingleTrait, R>', not the expected contextual result type 'PrimitiveSequence<SingleTrait, _>'

Self contained code example that reproduces the issue:

func convertExample() -> Completable {
    return Single<String>.create { single in
        single(.success("hello"))
        return Disposables.create()
      }
      .flatMap({ _ -> Completable in
        return Completable.empty()
      })
  }

RxSwift/RxCocoa/RxBlocking/RxTest version/commit

v3.4.0

Platform/Environment

  • iOS
  • macOS
  • tvOS
  • watchOS
  • playgrounds

How easy is to reproduce? (chances of successful reproduce after running the self contained code)

  • easy, 100% repro
  • sometimes, 10%-100%
  • hard, 2% - 10%
  • extremely hard, %0 - 2%

Xcode version:

8.3.1

⚠️ Fields below are optional for general issues or in case those questions aren’t related to your issue, but filling them out will increase the chances of getting your issue resolved. ⚠️

Installation method:

  • CocoaPods
  • Carthage
  • Git submodules

I have multiple versions of Xcode installed: (so we can know if this is a potential cause of your issue)

  • yes (which ones)
  • no

Level of RxSwift knowledge: (this is so we can understand your level of knowledge and formulate the response in an appropriate manner)

  • just starting
  • I have a small code base
  • I have a significant code base

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 3
  • Comments: 35 (13 by maintainers)

Most upvoted comments

I absolutely agree with @tarunon. I encounter that situation almost every day.

Currently I implement that architecture by this way,

func createComplexCompletable() -> Completable {
    return singleA
        .asObservable()
        .flatMap { valueA -> Observable<Never> in
            otherCreateCompletableMethod(arg: valueA) // <- returns Completable
                .asObservable()
        }
        .asCompletable()
}

Looks terrible. 🙃

If I can write

func createComplexCompletable() -> Completable {
    return singleA
        .flatMap { valueA -> Completable in
            otherCreateCompletableMethod(arg: valueA)
        }
}

Looks smart. 😎

Single should be able to be converted into Maybe and Completable, and Completable only to Maybe (But Maybe cannot be converted to other traits).

I wrote a code something like below and it’s working. It takes out the source observable once by asObservable and then convert it to other traits.

extension PrimitiveSequence where TraitType == SingleTrait {
    public func asMaybe() -> PrimitiveSequence<MaybeTrait, Element> {
        return self.asObservable().asMaybe()
    }
    
    public func asCompletable() -> PrimitiveSequence<CompletableTrait, Never> {
        return self.asObservable().flatMap { _ in Observable<Never>.empty() }.asCompletable()
    }
}

extension PrimitiveSequence where TraitType == CompletableTrait, ElementType == Swift.Never {
    public func asMaybe() -> PrimitiveSequence<MaybeTrait, Element> {
        return self.asObservable().asMaybe()
    }
}

I’ve thought about this a bit, we can do something similar like RxJava did, it makes sense, but it might take a month or two to release it.

In my opinion, that make sense next flatMap overloading.

Source\Closure (T) -> Single<U> (T) -> Maybe<U> (T) -> Completable
Single<T> Single<U> Maybe<U> Completable
Maybe<T> Maybe<U> Maybe<U> Completable

Completable has no next(success), so there is no implementation of flatMap

We often make the function that has 1 argument and return Completable, but if we wish to use this function in PrimitiveSequence chaining, I think there is no way to use this function. andThen can connect from Single to Completable, but we cannot receive next(success) value.

How do you think? @kzaher

Actually I believe that “flatmapCompletable” is totally necessary. In my case, I receive a stream from which I want to use data (Single), but the stream that goes next must not emit any value (Completable). Example:

private func addToken<T>(request : RequestBuilder<T>) -> Single<RequestBuilder<T>> {
        return Single.create {
            request.addHeaders([TokenRepository.headerKey : TokenRepository.sharedInstance.token!])
            $0(.success(request))
            return Disposables.create()
        }
    }

func secureRequest(request : RequestBuilder<Void>) -> Completable {
        return self.addToken(request: request)
            .flatMap {
                self.executeRequest(request: $0)
        }
    }

func executeRequest(request : RequestBuilder<Void>) -> Completable {
        return Completable.create(subscribe: { (observer) -> Disposable in
            request.execute({ (response, error) in
                    guard response != nil else {
                        let error = error ?? WebServiceError.unknown
                        observer(.error(error))
                        return
                    }
                    observer(.completed)
                })
            return Disposables.create()
        })
    }

I have been using RxJava2 and I think they manage better what is related to the changes between observable types than RxSwift… Single to Maybe, Single to Completable, flatmapCompletable, etc.

@freak4pc

public func ignoreElements()
        -> Completable

What about, for example, network requests that yields just success or error? The Completable trait is the best representation of an operation that just completes or fails, although I recognize that the issue making Completables able to be flatMap because of the main reason that they not produce any next event is understandable… Having network requests of Single<Void> really cringes me… I can’t see a correct approach to this, if someone does I’ll be grateful.

Thanks for your opinion 😃

The only point I’d like to “point on” is that Completable has a contextual meaning.

Single means a Single element Maybe means a Single element or No Elements and Completable means No Elements

Completable is the “Correct” Rx terminology declared by ReactiveX, and the fact we have Never in swift doesn’t mean it needs to be hard to convert Observables to any kind of trait (be it Single, Maybe or Completable).

Using Single<Void> works just as well but when I read a protocol that says “Completable”, the mental load is much lower - I immediately understand contextually this means it would never return any values and can only fail or complete.

Again, practically Single<Void> is the same, but contextually there is a slight difference.

Anyways I completely respect your will to keep the library slim which is why I added it as an extension on my own codebase.

Thanks for the kind explanation, as usual 😃

I also stumbled upon a similar issue: I have a bunch on Completable methods and I would like to chain them in a sane way. However, it seems that it is not possible at the moment.

Actually, you can just call .asObservable on the Single and then flatmap it to an Observable.

@HayTran94 Actually, we can write something slightly better:

func createComplexCompletable() -> Completable {
    return singleA
        .asObservable()
        .flatMap { valueA -> Completable in
            otherCreateCompletableMethod(arg: valueA) // <- returns Completable
        }
        .asCompletable()
}

Compiles fine on my machine.

@freak4pc I think the only discoverability part might be non optimal. I think as far as readability is concerned, it does exactly what name suggests as far as I can tell.

If a sequence is a Single or Maybe that proves that it has 1 and 0-1 elements respectively. That means that some operator would need to be performed on it to ignore the elements to make it Completable (which means that sequence doesn’t contain any element).

We’ve added asSingle because there isn’t a way to prove compile time from type description of Observable sequence that it contains just one element, so we error out Single when it doesn’t contain exactly one element.

In case of Completable we can prove compile time that Observable sequence doesn’t contain any elements (typed Never). In case it can return elements, there is an operator that ignores all of the elements and returns Completable.

Like RxJava, Completable has andThen operator if chaining is wanted.

So far I haven’t encountered any situation where using Completable is advantageous to Single<()>, so I wouldn’t like to bloat the library with a lot of additional operators.

I think this should work at this moment.

func secureRequest(request : RequestBuilder<Void>) -> Completable {
        return self.addToken(request: request)
            .flatMap {
                self.executeRequest(request: $0).asObservable()
        }.asCompletable()
    }

If this was modeled as Single.

func secureRequest(request : RequestBuilder<Void>) -> Single<()> {
        return self.addToken(request: request)
            .flatMap {
                self.executeRequest(request: $0)
        }
    }

a bit cleaner.

The ignoreElements() comment from @kzaher seems right, except perhaps we don’t want to change the existing api. I just hacked up the following extension using ignoreElements() and the existing asCompletable() that’s already implemented for Observable<Never>

// allow turning anything into a Completable ignoring all events
public extension ObservableType {
    public func asCompletableIgnoringEvents() -> Completable {
        return self.ignoreElements()
            .map { _ in preconditionFailure() }
            .asCompletable()
    }
}

@programmerdave However, it handles server errors nicely. materialize also wraps the Error into a normal next event so your pipe doesn’t burst when the server emits an error. Very nice…