Skip to content

Add combinator #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: add-error-coercion
Choose a base branch
from
Open

Add combinator #51

wants to merge 6 commits into from

Conversation

mikemedina
Copy link
Contributor

@mikemedina mikemedina commented Mar 26, 2025

It's common when validating multiple inputs (something like form validation) to have many possible results where you can only continue if they're all ok and you need to effectively short-circuit on any err.

Using Result::mapOk and Result::flatMapOk can be handy, but when you need to refer to the unwrapped ok values all within one scope, you end up with the "pyramid of doom" nested indentation:

public static Result<ExamplePrivilegeEgg, ExampleError> fromUrn(String urn) {
  String[] split = urn.split(":");
  if (split.length != 3) {
    return Result.err(ExampleError.INVALID_URN);
  }

  return Optional
    .ofNullable(Longs.tryParse(split[1]))
    .map(Result::<Long, ExampleError>ok)
    .orElseGet(() -> Result.err(ExampleError.INVALID_PRIVILEGE_ID))
    .flatMapOk(id ->
      EntityType
        .from(split[0])
        .flatMapOk(privilegeType ->
          GrantType
            .from(split[2])
            .flatMapOk(grantType ->
              Result.ok(
                // Here we need id, privilegeType, and grantType from the scopes above
                ExamplePrivilegeEgg
                  .builder()
                  .setEntityId(id)
                  .setEntityType(privilegeType)
                  .setGrantType(grantType)
                  .build()
              )
            )
        )
    );
}

An alternative approach is to check each intermediate result and return early with guard clauses which is much more readable but we end up having a pile of extra code blocks, isErr checks, and then having to unwrapOrElseThrow() a bunch of times at the end:

public static Result<ExamplePrivilegeEgg, ExampleError> fromUrn(String urn) {
  String[] split = urn.split(":");
  if (split.length != 3) {
    return Result.err(ExampleError.INVALID_URN);
  }

  Result<EntityType, ExampleError> entityResult = EntityType.from(split[0]);
  if (entityResult.isErr()) {
    return entityResult.coerceErr();
  }

  Result<GrantType, ExampleError> grantResult = GrantType.from(split[2]);
  if (grantResult.isErr()) {
    return grantResult.coerceErr();
  }

  Result<Long, ExampleError> idResult = Optional
    .ofNullable(Longs.tryParse(split[1]))
    .map(Result::<Long, ExampleError>ok)
    .orElseGet(() -> Result.err(ExampleError.INVALID_PRIVILEGE_ID));
  if (idResult.isErr()) {
    return idResult.coerceErr();
  }

  // unwrap, unwrap, unwrap, etc.
  return ExamplePrivilegeEgg.builder()
      .setEntityId(idResult.unwrapOrElseThrow())
      .setEntityType(entityResult.unwrapOrElseThrow())
      .setGrantType(grantResult.unwrapOrElseThrow())
      .build());
}

This PR adds a Result::combine utility method that let's you combine up to 9 separate results of any type, operate on them all within a single mapping function, and return a new result, short-circuiting if any of the input results contained errors.

public static Result<ExamplePrivilegeEgg, ExampleError> fromUrn(String urn) {
  String[] split = urn.split(":");
  if (split.length != 3) {
    return Result.err(ExampleError.INVALID_URN);
  }

  Result<EntityType, ExampleError> entityResult = EntityType.from(split[0]);
  Result<GrantType, ExampleError> grantResult = GrantType.from(split[2]);
  Result<Long, ExampleError> idResult = Optional
    .ofNullable(Longs.tryParse(split[1]))
    .map(Result::<Long, ExampleError>ok)
    .orElseGet(() -> Result.err(ExampleError.INVALID_PRIVILEGE_ID));

  // Clean, flat chain with safely typed, named parameters
  return Result.combine(idResult)
    .and(entityResult)
    .and(grantResult)
    .map((id, entityType, grantType) -> ExamplePrivilegeEgg
      .builder()
      .setEntityId(id)
      .setEntityType(entityType)
      .setGrantType(grantType)
      .build());
}
image

@mikemedina mikemedina marked this pull request as ready for review April 9, 2025 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant