What's the point of .switchIfEmpty() getting evaluated eagerly?
Asked Answered
G

3

9

Even if my stream is not empty, the fallback stream would always be created? What's the intent behind doing this? This is extremely non-idiomatic.

On the other hand, .onErrorResume is evaluated lazily.

Could someone please explain to me why .switchIsEmpty is evaluated eagerly?

Here's the code:

  public static void main(String[] args) {
    Mono<Integer> m = Mono.just(1);
    m.flatMap(a -> Mono.delay(Duration.ofMillis(5000)).flatMap(p -> Mono.empty()))
        .switchIfEmpty(getFallback())
        .doOnNext(a -> System.out.println(a))
        .block();
  }

  private static Mono<Integer> getFallback() {
    System.out.println("In Here");
    return Mono.just(5);
  }

The output is:

In Here (printed immediately)
5 (after 5s)
Gaikwar answered 10/9, 2019 at 12:21 Comment(4)
You probably made the mistake of doing initialization before returning a flow to be used by switchIfEmpty. Please provide the code you are having problems with.Octuple
@Octuple I have added the code. Thanks.Gaikwar
What happens if you just write getFallback(); in main without switchIfEmpty and the other constructs? Why?Octuple
The method is executed on the main thread? Looks like I am missing something fundamental.Gaikwar
N
21

What you need to understand here is the difference between assembly time and subscription time.

Assembly time is when you create your pipeline by building the operator chain. At this point your publisher is not subscribed yet and you need to think kind of imperatively.

Subscription time is when you trigger the execution by subscribing and the data starts flow through your pipeline. This is when you need to think reactively in terms of callbacks, lambdas, lazy execution, etc..

More on this in the great article by Simon Baslé.

As @akarnokd mentioned in his answer, the getFallback() method is called imperatively at assembly time since it is not defined as a lambda, just a regular method call.

You can achieve true laziness by one of the below methods:

1, You can use Mono.fromCallable and put your log inside the lambda:

public static void main(String[] args) {
    Mono<Integer> m = Mono.just(1);

    m.flatMap(a -> Mono.delay(Duration.ofMillis(5000)).flatMap(p -> Mono.empty()))
     .switchIfEmpty(getFallback())
     .doOnNext(a -> System.out.println(a))
     .block();
}

private static Mono<Integer> getFallback() {
    System.out.println("Assembly time, here we are just in the process of creating the mono but not triggering it. This is always called regardless of the emptiness of the parent Mono.");
    return Mono.fromCallable(() -> {
        System.out.println("Subscription time, this is the moment when the publisher got subscribed. It is got called only when the Mono was empty and fallback needed.");
        return 5;
    });
}

2, You can use Mono.defer and delay the execution and the assembling of your inner Mono until subscription:

public static void main(String[] args) {
    Mono<Integer> m = Mono.just(1);
    m.flatMap(a -> Mono.delay(Duration.ofMillis(5000)).flatMap(p -> Mono.empty()))
     .switchIfEmpty(Mono.defer(() -> getFallback()))
     .doOnNext(a -> System.out.println(a))
     .block();
}

private static Mono<Integer> getFallback() {
    System.out.println("Since we are using Mono.defer in the above pipeline, this message gets logged at subscription time.");
    return Mono.just(5);
}

Note that your original solution is also perfectly fine. You just need to aware of that the code before returning the Mono is executed at assembly time.

Narcotic answered 10/9, 2019 at 20:12 Comment(0)
O
4

If you put parenthesis around it, why would it execute anywhere else? This type of misunderstanding comes up quite often and not sure what the source is.

What happens should become more apparent when your code is rewritten:

Mono<Integer> m = Mono.just(1);
Mono<Integer> m2 = m.flatMap(a -> Mono.delay(Duration.ofMillis(5000))
                                      .flatMap(p -> Mono.empty()));

Mono<Integer> theFallback = getFallback(); // <------------------ still on the main thread!

m2.switchIfEmpty(theFallback)
    .doOnNext(a -> System.out.println(a))
    .block();

getFallback runs because its parent method is executing right there. This has nothing to do with Reactive Programming but is a fundamental property of most programming languages.

Octuple answered 10/9, 2019 at 12:57 Comment(0)
F
2

This strongly reminders me of java.util.Optional. For example:

String input = "not null"; // change to null

String result = Optional.ofNullable(input)
            .orElse(fallback());

System.out.println(result);

private static String fallback() {
    System.out.println("inside fallback");
    return "fallback";
}

No matter the value of input (null or not), it still evaluates fallback method. Unlike Mono though, Optional offers orElseGet that is evaluated lazily, via a java.util.Function. Doing .switchIfEmpty(Mono.defer(() -> getFallback())) is weird, at best, imo.

Fairish answered 13/8, 2022 at 11:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.