arrow icon indicating copy to clipboard operation
arrow copied to clipboard

["Request"] more readable alternative to zipOrAccumulate using Kotlin contracts

Open Intex32 opened this issue 1 year ago • 5 comments

What version are you currently using? V1.2

What would you like to see? I want to have a more readable alternative to zipOrAccumulate, when doing error accumulation.

either {
    val a: ValidatedNel<String, String> = "coo".validNel()
    val b: ValidatedNel<String, String> = "kie".validNel()
    val c: ValidatedNel<String, String> = "mesomerism".validNel()

    ensureAllValid(a, b)

    a.value
    b.value
    //c.value // does not compile

    a.value + b.value
} shouldBeEqual "cookie".right()

Motivation: Currently, zipOrAccumulate works by taking in a number of lambdas whose return values are zipped using another trailing lambda (happy path). Arrow Docs zipOrAccumulate's flaws are:

  • much wrapping with lambdas
  • potential "redeclaration" of validated variables in the zip function
  • happy path is indented (less readable)

Furthermore this aligns well with current Either DSL (e.g. ensure).

Suggestion: Use Kotlin contracts to declare a certain variables as valid if they are otherwise accumulate and combine their errors.

Concrete solution's implementation:

suspend inline fun <ERROR, ACC_ERROR, reified A, reified B> EffectScope<ACC_ERROR>.ensureAllValid(a: ValidatedNel<ERROR, A>, b: ValidatedNel<ERROR, B>, noinline mapAccErrors: (Nel<ERROR>) -> ACC_ERROR) {
    contract {
        returns() implies (a is Valid<A> && b is Valid<B>)
    }
    ensureAllValidInternal(nonEmptyListOf(a, b), mapAccErrors)
}

suspend inline fun <ERROR, reified A, reified B> EffectScope<Nel<ERROR>>.ensureAllValid(a: ValidatedNel<ERROR, A>, b: ValidatedNel<ERROR, B>) {
    contract {
        returns() implies (a is Valid<A> && b is Valid<B>)
    }
    ensureAllValidInternal(nonEmptyListOf(a, b), ::identity)
}

@PublishedApi
internal suspend fun <ERROR, ACC_ERROR> EffectScope<ACC_ERROR>.ensureAllValidInternal(
    all: Nel<ValidatedNel<ERROR, Any?>>,
    mapAccErrors: (Nel<ERROR>) -> ACC_ERROR,
) {
    val invalids = all.filterIsInstance<Invalid<Nel<ERROR>>>().toNonEmptyListOrNone()
        .getOrElse { return }
    val accumulatedErrors = invalids
        .flatMap { it.value }
        .let(mapAccErrors)
    shift<Nothing>(accumulatedErrors)
}

This of course would need to be repetitively scaled to a certain number of arguments. The example given consider just two arguments.

Current Limitations:

  • contracts still experimental
  • contracts are unstable and unreliable at the time being
  • in some cases I had compiler errors and had to recompile the entire project (I haven't figured out in detail yet, what the exact cause is)

Intex32 avatar Jun 21 '23 09:06 Intex32

If I understand correctly, what you want here is kind of "accumulate as much as possible" scenario. The shape of zipOrAccumulate ensures that all the validations can be independently executed, but this is no longer possible to guarantee if we're just using a block.

serras avatar Jul 06 '23 08:07 serras

@serras I'm afraid you didn't get the point. The observed behavior of my proposal should be the same as for zipOrAccumulate, just another syntax so to speak. After the ensureAllValid call - as the name suggests - all variables passed as parameters are guaranteed to be Valid. Also, in what sense do you mean independent?

Intex32 avatar Jul 08 '23 14:07 Intex32

As Validated has been deprecated and will be removed, I think your example could be rewritten using EitherNel, right? And ensureAllRight would be your magic ingredient for that one point where all Eithers are checked. Maybe it should more be like a bindAll? But that one would return a bunch of values as a return value which could be destructured?

Zordid avatar Aug 02 '23 04:08 Zordid

@Zordid Greetings from Munich, Yes, as Validated is deprecated, this can be transferred to EitherNel, afaik.

bindAll is slightly different. All Iterable's elements would have to be of type A which is a heavy restriction. Also, destructuring implies redeclaring all variables which often times (at least for me) results in duplicate names (one for the original and one for the validated). Thus, I view my proposal as much more convenient.

image

Intex32 avatar Aug 03 '23 19:08 Intex32

We've managed to create a DSL in this style in https://github.com/arrow-kt/arrow/pull/3436

serras avatar Jun 14 '24 18:06 serras