LogoLogo
GitHubFree book
  • 🏠Introduction
  • Using the Library
    • ðŸŒąGetting Started
      • Adding Result to Your Build
      • Creating Results
    • ðŸŠīBasic Usage
      • Checking Success or Failure
      • Unwrapping Values
      • Conditional Actions
    • 🚀Advanced Usage
      • Screening Results
      • Transforming Results
    • 🏁Recap
  • Add-ons
    • ðŸ’ĪLazy Results
    • ðŸ—ĢïļFluent Assertions
    • 📜Jackson Module
    • 🧑‍🚀Micronaut Serialization
  • Other resources
    • ðŸ“ĶBill of Materials
    • 📈Benchmarks
    • ðŸĪ–Demo Projects
      • Spring Boot Demo Project
      • Micronaut Demo Project
    • ⚖ïļLicense
Powered by GitBook
LogoLogo

Source Code

  • GitHub
  • License

Quality

  • SonarCloud
  • Benchmarks

Documentation

  • Free book
  • Javadoc

Releases

  • Maven Central
  • Bill of Materials

Copyright 2024 Guillermo Calvo

On this page
  • How to Use this Add-On
  • Creating Lazy Results
  • Skipping Expensive Calculations
  • Triggering Result Evaluation
  • Handling Success and Failure Eagerly
  • Handling Success and Failure Lazily
  • Conclusion

Was this helpful?

Edit on GitHub
Export as PDF
  1. Add-ons

Lazy Results

How to defer expensive calculations with Results

PreviousRecapNextFluent Assertions

Last updated 6 months ago

Was this helpful?

Lazy results optimize performance by deferring costly operations until absolutely necessary. They behave like regular results, but only execute the underlying operation when an actual check for success or failure is performed.

How to Use this Add-On

Add this Maven dependency to your build:

Group ID
Artifact ID
Latest Version

com.leakyabstractions

result-lazy

provides snippets for different build tools to declare this dependency.

Creating Lazy Results

We can use to create a lazy result.

Supplier<Result<Integer, String>> supplier = () -> success(123);
Result<Integer, String> lazy = LazyResults.ofSupplier(supplier);

While can return a fixed success or failure, lazy results shine when they encapsulate time-consuming or resource-intensive operations.

/* Represents the operation we may omit */
Result<Long, Exception> expensiveCalculation(AtomicLong timesExecuted) {
  long counter = timesExecuted.incrementAndGet();
  return success(counter);
}

This sample method simply increments and returns a counter for brevity. However, in a typical scenario, this would involve an I/O operation.

Skipping Expensive Calculations

@Test
void shouldSkipExpensiveCalculation() {
  AtomicLong timesExecuted = new AtomicLong();
  // Given
  Result<Long, Exception> lazy = LazyResults
      .ofSupplier(() -> expensiveCalculation(timesExecuted));
  // When
  Result<String, Exception> transformed = lazy.mapSuccess(Object::toString);
  // Then
  assertNotNull(transformed);
  assertEquals(0L, timesExecuted.get());
}

In this example, the expensive calculation is omitted because the lazy result is never fully evaluated. This test demonstrates that a lazy result can be transformed while maintaining laziness, ensuring that the expensive calculation is deferred.

These methods will preserve laziness:

Triggering Result Evaluation

@Test
void shouldExecuteExpensiveCalculation() {
  AtomicLong timesExecuted = new AtomicLong();
  // Given
  Result<Long, Exception> lazy = LazyResults
      .ofSupplier(() -> expensiveCalculation(timesExecuted));
  // When
  Result<String, Exception> transformed = lazy.mapSuccess(Object::toString);
  boolean success = transformed.hasSuccess();
  // Then
  assertTrue(success);
  assertEquals(1L, timesExecuted.get());
}

Here, the expensive calculation is executed because the lazy result is finally evaluated.

Terminal methods will immediately evaluate the lazy result:

Handling Success and Failure Eagerly

@Test
void shouldHandleSuccessEagerly() {
  AtomicLong timesExecuted = new AtomicLong();
  AtomicLong consumerExecuted = new AtomicLong();
  Consumer<Long> consumer = x -> consumerExecuted.incrementAndGet();
  // Given
  Result<Long, Exception> lazy = LazyResults
      .ofSupplier(() -> expensiveCalculation(timesExecuted));
  // When
  lazy.ifSuccess(consumer);
  // Then
  assertEquals(1L, timesExecuted.get());
  assertEquals(1L, consumerExecuted.get());
}

In this test, we don't explicitly unwrap the value or check the status, but since we want to consume the success value, we need to evaluate the lazy result first.

Furthermore, even if we wanted to handle the failure scenario, we would still need to evaluate the lazy result.

@Test
void shouldHandleFailureEagerly() {
  AtomicLong timesExecuted = new AtomicLong();
  AtomicLong consumerExecuted = new AtomicLong();
  Consumer<Exception> consumer = x -> consumerExecuted.incrementAndGet();
  // Given
  Result<Long, Exception> lazy = LazyResults
      .ofSupplier(() -> expensiveCalculation(timesExecuted));
  // When
  lazy.ifFailure(consumer);
  // Then
  assertEquals(1L, timesExecuted.get());
  assertEquals(0L, consumerExecuted.get());
}

These methods are treated as terminal when used with regular consumer functions:

Handling Success and Failure Lazily

@Test
void shouldHandleSuccessLazily() {
  AtomicLong timesExecuted = new AtomicLong();
  AtomicLong consumerExecuted = new AtomicLong();
  Consumer<Long> consumer = LazyConsumer
      .of(x -> consumerExecuted.incrementAndGet());
  // Given
  Result<Long, Exception> lazy = LazyResults
      .ofSupplier(() -> expensiveCalculation(timesExecuted));
  // When
  lazy.ifSuccess(consumer);
  // Then
  assertEquals(0L, timesExecuted.get());
  assertEquals(0L, consumerExecuted.get());
}

Conclusion

We learned how to defer expensive calculations until absolutely necessary. By leveraging lazy results, you can optimize performance by avoiding unnecessary computations and only evaluating the operation's outcome when needed.

The advantage of lazy results is that they defer invoking the provided for as long as possible. Despite this, you can screen and transform them like any other result without losing their laziness.

Finally, when it's time to check whether the operation succeeds or fails, the lazy result will execute it. This is triggered by using any of the terminal methods, such as .

By default, , , and are treated as terminal methods. This means they eagerly evaluate the result and then perform an action based on its status.

In this other test, we use instead of . Since the lazy result is evaluated to a success, the failure consumer is never executed.

When these conditional actions may also be skipped along with the expensive calculation, we can encapsulate them into a instead of a regular . All we need to do is to create the consumer using . Lazy consumers will preserve the laziness until a terminal method is eventually used on the result.

Here, we use a lazy consumer with so the expensive calculation is skipped because the lazy result is never fully evaluated.

The full source code for the examples is .

ðŸ’Ī
Maven Central
LazyResults::ofSupplier
suppliers
Supplier
Result::filter
Result::recover
Result::mapSuccess
Result::mapFailure
Result::map
Result::flatMapSuccess
Result::flatMapFailure
Result::flatMap
Result::hasSuccess
Result::hasSuccess
Result::hasFailure
Result::getSuccess
Result::getFailure
Result::orElse
Result::orElseMap
Result::streamSuccess
Result::streamFailure
Result::ifSuccess
Result::ifFailure
Result::ifSuccessOrElse
Result::ifFailure
Result::ifSuccess
Result::ifSuccess
Result::ifFailure
Result::ifSuccessOrElse
LazyConsumer
Consumer
LazyConsumer::of
Result::ifSuccess
available on GitHub