Search code examples
kotlinfunctional-programmingarrow-kt

Understanding Validated.applicative in kotlin arrow library


I come across below generic function which takes two Either type and a function as an argument. If both arguments are Either.Right then apply the function over it and returns the result, if any of the argument is Either.Left it returns NonEmptyList(Either.Left). Basically it performs the independent operation and accumulates the errors.

fun <T, E, A, B> constructFromParts(a: Either<E, A>, b: Either<E, B>, fn: (Tuple2<A, B>) -> T): Either<Nel<E>, T> {
    val va = Validated.fromEither(a).toValidatedNel()
    val vb = Validated.fromEither(b).toValidatedNel()
    return Validated.applicative<Nel<E>>(NonEmptyList.semigroup()).map(va, vb, fn).fix().toEither()
}

val error1:Either<String, Int> = "error 1".left()
val error2:Either<String, Int> = "error 2".left()

val valid:Either<Nel<String>, Int> = constructFromParts(
        error1,
        error2
){(a, b) -> a+b}

fun main() {
    when(valid){
        is Either.Right -> println(valid.b)
        is Either.Left -> println(valid.a.all)
    }
}

Above code prints

[error 1, error 2]

Inside the function, it converts Either to ValidatedNel type and accumulates both errors ( Invalid(e=NonEmptyList(all=[error 1])) Invalid(e=NonEmptyList(all=[error 2])) )

My question is how it performs this operation or could anyone explain the below line from the code.

return Validated.applicative<Nel<E>>(NonEmptyList.semigroup()).map(va, vb, fn).fix().toEither()

Solution

  • Let's say I have a similar data type to Validated called ValRes

    sealed class ValRes<out E, out A> {
        data class Valid<A>(val a: A) : ValRes<Nothing, A>()
        data class Invalid<E>(val e: E) : ValRes<E, Nothing>()
    }
    

    If I have two values of type ValRes and I want to combine them accumulating the errors I could write a function like this:

    fun <E, A, B> tupled(
                a: ValRes<E, A>,
                b: ValRes<E, B>,
                combine: (E, E) -> E
            ): ValRes<E, Pair<A, B>> =
                if (a is Valid && b is Valid) valid(Pair(a.a, b.a))
                else if (a is Invalid && b is Invalid) invalid(combine(a.e, b.e))
                else if (a is Invalid) invalid(a.e)
                else if (b is Invalid) invalid(b.e)
                else throw IllegalStateException("This is impossible")
    
    • if both values are Valid I build a pair of the two values
    • if one of them is invalid, I get a new Invalid instance with the single value
    • if both are invalid, I use the combine function to build Invalid instance containing both values.

    Usage:

    tupled(
        validateEmail("stojan"),    //invalid
        validateName(null)          //invalid
    ) { e1, e2 -> "$e1, $e2" }
    

    This works in a generic way, independent of the types E, A and B. But it only works for two values. We could build such a function for N values of type ValRes.

    Now back to arrow:

    Validated.applicative<Nel<E>>(NonEmptyList.semigroup()).map(va, vb, fn).fix().toEither()
    

    tupled is similar to map (with hardcoded success function). va and vb here are similar to a and b in my example. Instead of returning a pair of values, here we have a custom function (fn) that combines the two values in case of success.

    Combining the errors:

    interface Semigroup<A> {
      /**
       * Combine two [A] values.
       */
      fun A.combine(b: A): A
    }
    
    

    Semigroup in arrow is a way for combining two values from the same type in a single value of that same type. Similar to my combine function. NonEmptyList.semigroup() is the implementation of Semigroup for NonEmptyList that given two lists adds the elements together into a single NonEmptyList.

    To sum up:

    • If both values are Valid -> it will combine them using the supplied function
    • If one value is Valid and one Invalid -> gives back the error
    • If both values are Invalid -> Uses the Semigroup instance for Nel to combine the errors

    Under the hood this scales for 2 up to X values (22 I believe).