Skip to content
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

Allow bindNel in mapOrAccumulate, and add missing signatures #2952

Merged
merged 14 commits into from
Mar 6, 2023

Conversation

nomisRev
Copy link
Member

@nomisRev nomisRev commented Feb 27, 2023

This PR adds missing signatures of mapOrAccumulate alongside https://github.com/arrow-kt/arrow/pull/2940/files & #2935.

Our mapOrAccumulate was currently not compatible with EitherNel (or ValidatedNel), and this adds some friction when accumulating errors. There are several solution to this problem:

  1. Redefine mapOrAccumulateNel over Raise<NonEmptyList<E>> instead of Raise<E> and require callers to transform from E to NonEmptyList<E> before calling bind or raise using either.toEitherNel() or mapErrorNel { }. This results in the Validated deprecation to look like this for traverse:

  2. Introduce mapOrAccumulateNel, it needs to disambiguate in name or there is ambiguity in the API and the compiler cannot select the correct one based on @BuilderInference. (Can follow similar source -and binary compatible way migration post context-receivers). Requires explicitly calling mapOrAccumulate or mapOrAccumulateNel, and still requires explicit transformation using using either.toEitherNel() or mapErrorNel { } in cases where you need to mix-and-match.

  3. Introduce AccumulatingRaise which simulates the below context receivers based code. The upside here is that E and NonEmptyList<E> work seamlessly together, but the downside is that it introduces some minor issues with type interference, and in some cases the users will have to explicitly specify the type arguments. This is the case when E cannot be inferred by surrounding code, @BuilderInference is partially broken in this case due to ambiguity between Either<E>.bind and EitherNel<E>.bind.

context(Raise<NonEmptyList<Error>>)
fun <Error, A> Either<Error, A>.bind(): A = when (this) {
  is Invalid -> raise(nonEmptyListOf(value))
  is Valid -> value
}

@OptIn(ExperimentalTypeInference::class)
public inline fun <Error, A, B> Iterable<A>.mapOrAccumulate(
  @BuilderInference transform: Raise<NonEmptyList<Error>>.(A) -> B,
): Either<NonEmptyList<Error>, List<B>>

Side-notes

Inference problems

If you call bind on Validated<E, _> or Either<E, _> then @BuilderInference works, similarly for raise("test").
Alternatively if you call bind or raise on NonEmptyList<E> then it causes disambiguity, and you need to disambiguate. Disambiguating by specifying a return type only works if you don't chain another function like toValidated or map, in those cases you need to explicitly specify the type arguments. Explicitly specifying the type arguments always solves the disambiguity problem.

Future proofing

In option 2, and 3 we can migrate to context-receivers once they are released by marking mapOrAccumulate with AccumulatingRaise as @Deprecation(DeprecationLevel.HIDDEN) and introducing the above defined code. This will then be binary -and source compatible. All that would be needed is an additional import to arrow.core.raise.bind, this can be easily automated with OpenRewrite or ReplaceWith.

@nomisRev nomisRev added the 1.2.0 Tickets belonging to 1.1.2 label Feb 27, 2023
Copy link
Member

@serras serras left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nomisRev
Copy link
Member Author

I'm not sure what is the best intermediate solution until context receivers land. All feedback is very welcome, thoughts or ideas. And ask any and all questions if you have anything or the explanation in the description is not clear 🙏

@nomisRev nomisRev requested review from serras and raulraja February 27, 2023 19:05
@Zordid
Copy link
Contributor

Zordid commented Mar 2, 2023

The most important part in this migration is to make sure all old functions that will behave differently but compile fine are deprecated with explanations and/or ReplaceWiths. traverse is such a candidate that needs deprecation and replace with mapOrAccumulate. Otherwise people miss the altered behaviour in error accumulation. Thanks, @nomisRev !

@nomisRev
Copy link
Member Author

nomisRev commented Mar 2, 2023

Thanks for raising this @Zordid. Turned this into a ticket, #2955

Copy link
Member

@raulraja raulraja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed some if this PR offline with Simon, thanks for the clarifications

@nomisRev
Copy link
Member Author

nomisRev commented Mar 6, 2023

The BuilderInference issues have been levitated by introducing bindNel and RaiseAccumulate extending Raise<E> instead of Raise<Nel<E>> as suggested by @Zordid on Slack 🙌

Screenshot 2023-03-06 at 15 29 08

Copy link
Member

@serras serras left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this PR! (as much as you can love code, of course)

@nomisRev nomisRev changed the title Add missing mapOrAccumulate Allow bindNel in mapOrAccumulate, and add missing signatures Mar 6, 2023
@nomisRev nomisRev merged commit 656940c into main Mar 6, 2023
@nomisRev nomisRev deleted the sv-missing-mapOrAccumulate branch March 6, 2023 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1.2.0 Tickets belonging to 1.1.2 discussion help wanted
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants