Transforming Results

How to transform values wrapped inside Results

Transforming result objects is a key feature that enables you to compose complex operations in a clean and functional style. There are two primary techniques used for these transformations.

Mapping Results

Mapping involves applying a function to the value inside a result to produce a new result object.

Mapping Success Values

We can use Result::mapSuccess to apply a function to the success value of a result, transforming it into a new success value. If the result is a failure, it remains unchanged.

@Test
void testMapSuccess() {
  // Given
  Result<String, ?> result = success("HELLO");
  // When
  Result<Integer, ?> mapped = result.mapSuccess(String::length);
  // Then
  assertEquals(5, mapped.orElse(null));
}

In this example, we wrap a String inside a Result object and invoke mapSuccess to calculate its length and wrap it inside a new Result object.

Mapping Failure Values

Next up, we can use Result::mapFailure to apply a function to the failure value, transforming it into a new one. If the result is a success, it remains unchanged.

@Test
void testMapFailure() {
  // Given
  Result<?, BigDecimal> result = failure(ONE);
  // When
  Result<?, Boolean> mapped = result.mapFailure(TWO::equals);
  // Then
  assertFalse(mapped.getFailure().orElse(null));
}

Here, we invoke mapFailure to transform the failure type of the result from String to Boolean for demonstration purposes.

Mapping Both Success and Failure

The Result::map method simultaneously handles both success and failure cases by applying two separate functions: one for transforming the success value and one for transforming the failure value.

@Test
void testMap() {
  // Given
  Result<String, BigDecimal> result1 = success("HELLO");
  Result<String, BigDecimal> result2 = failure(ONE);
  // When
  Result<Integer, Boolean> mapped1 = result1.map(String::length, TWO::equals);
  Result<Integer, Boolean> mapped2 = result2.map(String::length, TWO::equals);
  // Then
  assertEquals(5, mapped1.orElse(null));
  assertFalse(mapped2.getFailure().orElse(null));
}

Flat-Mapping Results

Flat-mapping is used to chain operations that return results themselves, flattening the nested structures into a single result object. This allows you to transform a success into a failure, or a failure into a success.

To illustrate flat-mapping concepts, the next examples will follow a familiar "pet store" theme. This involves three Java types: Pet, PetError, and PetStore. These types will help us demonstrate the effective use of flat-mapping methods.

enum PetError {NOT_FOUND, NO_CONFIG}

record Pet(long id, String name) {

  static final Pet DEFAULT = new Pet(0, "Default pet");
  static final Pet ROCKY = new Pet(1, "Rocky");
  static final Pet GARFIELD = new Pet(2, "Garfield");
}

record PetStore(Pet... pets) {

  PetStore() {
    this(Pet.ROCKY, Pet.GARFIELD);
  }

  Result<Pet, PetError> find(long id) {
    Optional<Pet> found = stream(pets).filter(pet -> pet.id() == id).findAny();
    return Results.ofOptional(found, NOT_FOUND);
  }

  Result<Pet, PetError> getDefaultPet(PetError error) {
    return error == NO_CONFIG ? success(Pet.DEFAULT) : failure(error);
  }

  Result<Long, PetError> getDefaultPetId(PetError error) {
    return getDefaultPet(error).mapSuccess(Pet::id);
  }
}

With these types defined, we'll explore how to use various flat-mapping methods to transform result objects and manage pet-related operations in our imaginary pet store.

Flat-Mapping Successful Results

Use Result::flatMapSuccess to chain an operation that returns a result object. This method applies a mapping function to the success value, replacing the original result with the new one returned by the function. If the result is a failure, it remains unchanged.

@Test
void testFlatMapSuccess() {
  // Given
  PetStore store = new PetStore();
  Result<Long, PetError> result = success(100L);
  // When
  Result<Pet, PetError> mapped = result.flatMapSuccess(store::find);
  // Then
  assertEquals(NOT_FOUND, mapped.getFailure().orElse(null));
}

This example starts with a successful result containing a wrong pet ID (not found in the pet store). When we flat-map it with the store's find method reference, the final result contains a pet error.

Flat-Mapping Failed Results

Use Result::flatMapFailure to chain a result-bearing operation. This method also replaces the original result with the new one returned by the mapping function. If the result is a success, it remains unchanged.

@Test
void testFlatMapFailure() {
  // Given
  PetStore store = new PetStore();
  Result<Long, PetError> result = failure(NO_CONFIG);
  // When
  Result<Long, PetError> mapped = result.flatMapFailure(store::getDefaultPetId);
  // Then
  assertEquals(Pet.DEFAULT.id(), mapped.orElse(null));
}

Here we start with a failed result containing a pet error. When we flat-map it with the store's getDefaultPetId method reference, the final result contains the ID of the default pet in the store.

Flat-Mapping Both Success and Failure

The Result::flatMap method handles both success and failure cases by applying the appropriate function based on the status of the original result.

@Test
void testFlatMap() {
  // Given
  PetStore store = new PetStore();
  Result<Long, PetError> result1 = success(100L);
  Result<Long, PetError> result2 = failure(NO_CONFIG);
  // When
  Result<Pet, PetError> mapped1 = result1.flatMap(store::find, store::getDefaultPet);
  Result<Pet, PetError> mapped2 = result2.flatMap(store::find, store::getDefaultPet);
  // Then
  assertEquals(NOT_FOUND, mapped1.getFailure().orElse(null));
  assertEquals(Pet.DEFAULT, mapped2.orElse(null));
}

This example starts with a successful result containing a wrong pet ID (not found in the pet store). When we flat-map it with the store's find method reference, the final result contains a pet error.

Here we start with a failed result containing a pet error. When we flat-map it with the store's getDefaultPetId method reference, the final result contains the ID of the default pet in the store.

Conclusion

We demonstrated how to transform results in a concise and functional manner, enhancing the clarity and flexibility of your error-handling and data-processing logic.

Last updated

Logo

Copyright 2024 Guillermo Calvo