arrow-core-data / arrow.core / Validated

Validated

sealed class Validated<out E, out A> : ValidatedOf<E, A>

Imagine you are filling out a web form to sign up for an account. You input your username and password, then submit. A response comes back saying your username can’t have dashes in it, so you make some changes, then resubmit. You can’t have special characters either. Change, resubmit. Password needs to have at least one capital letter. Change, resubmit. Password needs to have at least one number.

Or perhaps you’re reading from a configuration file. One could imagine the configuration library you’re using returns an Either. Your parsing may look something like:

import arrow.core.Either
import arrow.core.Left
import arrow.core.flatMap

//sampleStart
data class ConnectionParams(val url: String, val port: Int)

fun <A> config(key: String): Either<String, A> = Left(key)

config<String>("url").flatMap { url ->
 config<Int>("port").map { ConnectionParams(url, it) }
}
//sampleEnd
// Either.Left(url)

You run your program and it says key “url” not found. Turns out the key was “endpoint.” So you change your code and re-run. Now it says the “port” key was not a well-formed integer.

It would be nice to have all of these errors reported simultaneously. The username’s inability to have dashes can be validated separately from it not having special characters, as well as from the password needing to have certain requirements. A misspelled (or missing) field in a config can be validated separately from another field not being well-formed.

Enter Validated.

Parallel Validation

Our goal is to report any and all errors across independent bits of data. For instance, when we ask for several pieces of configuration, each configuration field can be validated separately from one another. How then do we ensure that the data we are working with is independent? We ask for both of them up front.

As our running example, we will look at config parsing. Our config will be represented by a Map<String, String>. Parsing will be handled by a Read type class - we provide instances only for String and Int for brevity.

//sampleStart
abstract class Read<A> {

abstract fun read(s: String): A?

 companion object {

  val stringRead: Read<String> =
   object: Read<String>() {
    override fun read(s: String): String? = s
   }

  val intRead: Read<Int> =
   object: Read<Int>() {
    override fun read(s: String): Int? =
     if (s.matches(Regex("-?[0-9]+"))) s.toInt() else null
   }
 }
}
//sampleEnd

Then we enumerate our errors. When asking for a config value, one of two things can go wrong: The field is missing, or it is not well-formed with regards to the expected type.

sealed class ConfigError {
 data class MissingConfig(val field: String): ConfigError()
 data class ParseConfig(val field: String): ConfigError()
}

We need a data type that can represent either a successful value (a parsed configuration), or an error. It would look like the following, which Arrow provides in arrow.Validated:

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

Now we are ready to write our parser.

import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.Validated
import arrow.core.valid
import arrow.core.invalid

//sampleStart
data class Config(val map: Map<String, String>) {
 fun <A> parse(read: Read<A>, key: String): Validated<ConfigError, A> {
  val v = map[key]
  return when (v) {
   null -> Validated.Invalid(ConfigError.MissingConfig(key))
   else ->
    when (val s = read.read(v)) {
     null -> ConfigError.ParseConfig(key).invalid()
     else -> s.valid()
    }
  }
 }
}
//sampleEnd

And, as you can see, the parser runs sequentially: it first tries to get the map value and then tries to read it. It’s then straightforward to translate this to an effect block. We use here the either block which includes syntax to obtain A from values of Validated<*, A> through the arrow.core.computations.EitherEffect.invoke

import arrow.core.Validated
import arrow.core.computations.either
import arrow.core.valid
import arrow.core.invalid

//sampleStart
data class Config(val map: Map<String, String>) {
  suspend fun <A> parse(read: Read<A>, key: String) = either<ConfigError, A> {
    val value = Validated.fromNullable(map[key]) {
      ConfigError.MissingConfig(key)
    }.bind()
    val readVal = Validated.fromNullable(read.read(value)) {
      ConfigError.ParseConfig(key)
    }.bind()
    readVal
  }
}
//sampleEnd

Everything is in place to write the parallel validator. Remember that we can only do parallel validation if each piece is independent. How do we ensure the data is independent? By asking for all of it up front. Let’s start with two pieces of data.

import arrow.core.Validated
//sampleStart
fun <E, A, B, C> parallelValidate(v1: Validated<E, A>, v2: Validated<E, B>, f: (A, B) -> C): Validated<E, C> {
 return when {
  v1 is Validated.Valid && v2 is Validated.Valid -> Validated.Valid(f(v1.a, v2.a))
  v1 is Validated.Valid && v2 is Validated.Invalid -> v2
  v1 is Validated.Invalid && v2 is Validated.Valid -> v1
  v1 is Validated.Invalid && v2 is Validated.Invalid -> TODO()
  else -> TODO()
 }
}
//sampleEnd

We’ve run into a problem. In the case where both have errors, we want to report both. We don’t have a way to combine ConfigErrors. But, as clients, we can change our Validated values where the error can be combined, say, a List<ConfigError>. We are going to use a NonEmptyList<ConfigError>—the NonEmptyList statically guarantees we have at least one value, which aligns with the fact that, if we have an Invalid, then we most certainly have at least one error. This technique is so common there is a convenient method on Validated called toValidatedNel that turns any Validated<E, A> value to a Validated<NonEmptyList<E>, A>. Additionally, the type alias ValidatedNel<E, A> is provided.

Time to validate:

import arrow.core.NonEmptyList
import arrow.core.Validated
//sampleStart
fun <E, A, B, C> parallelValidate
  (v1: Validated<E, A>, v2: Validated<E, B>, f: (A, B) -> C): Validated<NonEmptyList<E>, C> =
 when {
  v1 is Validated.Valid && v2 is Validated.Valid -> Validated.Valid(f(v1.a, v2.a))
  v1 is Validated.Valid && v2 is Validated.Invalid -> v2.toValidatedNel()
  v1 is Validated.Invalid && v2 is Validated.Valid -> v1.toValidatedNel()
  v1 is Validated.Invalid && v2 is Validated.Invalid -> Validated.Invalid(NonEmptyList(v1.e, listOf(v2.e)))
  else -> throw IllegalStateException("Not possible value")
 }
//sampleEnd

Improving the validation

Kotlin says that our match is not exhaustive and we have to add else. To solve this, we would need to nest our when, but that would complicate the code. To achieve this, Arrow provides zip. This function combines Validateds by accumulating errors in a tuple, which we can then map. The above function can be rewritten as follows:

import arrow.core.Validated
import arrow.core.validNel
import arrow.core.zip
import arrow.typeclasses.Semigroup

//sampleStart
val parallelValidate =
   1.validNel().zip(Semigroup.nonEmptyList<ConfigError>(), 2.validNel())
    { a, b -> /* combine the result */}
//sampleEnd

Note that there are multiple zip functions with more arities, so we could easily add more parameters without worrying about the function blowing up in complexity.

When working with NonEmptyList in the Invalid side, there is no need to supply Semigroup as shown in the example above.

import arrow.core.Validated
import arrow.core.validNel
import arrow.core.zip

//sampleStart
val parallelValidate =
  1.validNel().zip(2.validNel())
    { a, b -> /* combine the result */}
//sampleEnd

Coming back to our example, when no errors are present in the configuration, we get a ConnectionParams wrapped in a Valid instance.

import arrow.core.Validated
import arrow.core.computations.either
import arrow.core.valid
import arrow.core.invalid
import arrow.core.NonEmptyList
import arrow.typeclasses.Semigroup

data class ConnectionParams(val url: String, val port: Int)

abstract class Read<A> {
 abstract fun read(s: String): A?

 companion object {

  val stringRead: Read<String> =
   object : Read<String>() {
    override fun read(s: String): String? = s
   }

  val intRead: Read<Int> =
   object : Read<Int>() {
    override fun read(s: String): Int? =
     if (s.matches(Regex("-?[0-9]+"))) s.toInt() else null
   }
 }
}

sealed class ConfigError {
 data class MissingConfig(val field: String) : ConfigError()
 data class ParseConfig(val field: String) : ConfigError()
}

data class Config(val map: Map<String, String>) {
  suspend fun <A> parse(read: Read<A>, key: String) = either<ConfigError, A> {
    val value = Validated.fromNullable(map[key]) {
      ConfigError.MissingConfig(key)
    }.bind()
    val readVal = Validated.fromNullable(read.read(value)) {
      ConfigError.ParseConfig(key)
    }.bind()
    readVal
  }.toValidatedNel()
}


suspend fun main() {
//sampleStart
 val config = Config(mapOf("url" to "127.0.0.1", "port" to "1337"))

 val valid = config.parse(Read.stringRead, "url").zip(
   Semigroup.nonEmptyList<ConfigError>(),
   config.parse(Read.intRead, "port")
 ) { url, port -> ConnectionParams(url, port) }
//sampleEnd
 println("valid = $valid")
}

But what happens when we have one or more errors? They are accumulated in a NonEmptyList wrapped in an Invalid instance.

import arrow.core.Validated
import arrow.core.computations.either
import arrow.core.valid
import arrow.core.invalid
import arrow.core.NonEmptyList

data class ConnectionParams(val url: String, val port: Int)

abstract class Read<A> {
 abstract fun read(s: String): A?

 companion object {

  val stringRead: Read<String> =
   object : Read<String>() {
    override fun read(s: String): String? = s
   }

  val intRead: Read<Int> =
   object : Read<Int>() {
    override fun read(s: String): Int? =
     if (s.matches(Regex("-?[0-9]+"))) s.toInt() else null
   }
 }
}

sealed class ConfigError {
 data class MissingConfig(val field: String) : ConfigError()
 data class ParseConfig(val field: String) : ConfigError()
}

data class Config(val map: Map<String, String>) {
  suspend fun <A> parse(read: Read<A>, key: String) = either<ConfigError, A> {
    val value = Validated.fromNullable(map[key]) {
      ConfigError.MissingConfig(key)
    }.bind()
    val readVal = Validated.fromNullable(read.read(value)) {
      ConfigError.ParseConfig(key)
    }.bind()
    readVal
  }.toValidatedNel()
}

suspend fun main() {
//sampleStart
val config = Config(mapOf("wrong field" to "127.0.0.1", "port" to "not a number"))

val valid = config.parse(Read.stringRead, "url").zip(
 Semigroup.nonEmptyList<ConfigError>(),
 config.parse(Read.intRead, "port")
) { url, port -> ConnectionParams(url, port) }
//sampleEnd
 println("valid = $valid")
}

Sequential Validation

If you do want error accumulation, but occasionally run into places where sequential validation is needed, then Validated provides a withEither method to allow you to temporarily turn a Validated instance into an Either instance and apply it to a function.

import arrow.core.Either
import arrow.core.flatMap
import arrow.core.left
import arrow.core.right
import arrow.core.Validated
import arrow.core.computations.either
import arrow.core.valid
import arrow.core.invalid

abstract class Read<A> {
 abstract fun read(s: String): A?

 companion object {

  val stringRead: Read<String> =
   object : Read<String>() {
    override fun read(s: String): String? = s
   }

  val intRead: Read<Int> =
   object : Read<Int>() {
    override fun read(s: String): Int? =
     if (s.matches(Regex("-?[0-9]+"))) s.toInt() else null
   }
 }
}

data class Config(val map: Map<String, String>) {
  suspend fun <A> parse(read: Read<A>, key: String) = either<ConfigError, A> {
    val value = Validated.fromNullable(map[key]) {
      ConfigError.MissingConfig(key)
    }.bind()
    val readVal = Validated.fromNullable(read.read(value)) {
      ConfigError.ParseConfig(key)
    }.bind()
    readVal
  }.toValidatedNel()
}

sealed class ConfigError {
 data class MissingConfig(val field: String) : ConfigError()
 data class ParseConfig(val field: String) : ConfigError()
}

//sampleStart
fun positive(field: String, i: Int): Either<ConfigError, Int> =
 if (i >= 0) i.right()
 else ConfigError.ParseConfig(field).left()

val config = Config(mapOf("house_number" to "-42"))

suspend fun main() {
  val houseNumber = config.parse(Read.intRead, "house_number").withEither { either ->
    either.flatMap { positive("house_number", it) }
  }
//sampleEnd
 println(houseNumber)
}

Types

Invalid data class Invalid<out E> : Validated<E, Nothing>
Valid data class Valid<out A> : Validated<Nothing, A>

Properties

isInvalid val isInvalid: Boolean
isValid val isValid: Boolean

Functions

all fun all(predicate: (A) -> Boolean): Boolean
bifoldLeft fun <B> bifoldLeft(c: B, fe: (B, E) -> B, fa: (B, A) -> B): B
bifoldMap fun <B> bifoldMap(MN: Monoid<B>, g: (E) -> B, f: (A) -> B): B
bifoldRight fun <B> ~~bifoldRight~~(c: Eval<B>, fe: (E, Eval<B>) -> Eval<B>, fa: (A, Eval<B>) -> Eval<B>): Eval<B>
bimap From arrow.typeclasses.Bifunctor, maps both types of this Validated.fun <EE, B> bimap(fe: (E) -> EE, fa: (A) -> B): Validated<EE, B>
bitraverse fun <EE, B> bitraverse(fe: (E) -> Iterable<EE>, fa: (A) -> Iterable<B>): List<Validated<EE, B>>
bitraverseEither fun <EE, B, C> bitraverseEither(fe: (E) -> Either<EE, B>, fa: (A) -> Either<EE, C>): Either<EE, Validated<B, C>>
exist Is this Valid and matching the given predicatefun exist(predicate: (A) -> Boolean): Boolean
findOrNull fun findOrNull(predicate: (A) -> Boolean): A?
fold fun <B> fold(fe: (E) -> B, fa: (A) -> B): B
foldLeft apply the given function to the value with the given B when valid, otherwise return the given Bfun <B> foldLeft(b: B, f: (B, A) -> B): B
foldMap fun <B> foldMap(MB: Monoid<B>, f: (A) -> B): B
foldRight fun <B> ~~foldRight~~(lb: Eval<B>, f: (A, Eval<B>) -> Eval<B>): Eval<B>
isEmpty fun isEmpty(): Boolean
isNotEmpty fun isNotEmpty(): Boolean
leftMap fun <EE> ~~leftMap~~(f: (E) -> EE): Validated<EE, A>
map Apply a function to a Valid value, returning a new Valid valuefun <B> map(f: (A) -> B): Validated<E, B>
mapLeft Apply a function to an Invalid value, returning a new Invalid value. Or, if the original valid was Valid, return it.fun <EE> mapLeft(f: (E) -> EE): Validated<EE, A>
show fun ~~show~~(SE: Show<E>, SA: Show<A>): String
swap fun swap(): Validated<A, E>
toEither Converts the value to an Either<E, A>fun toEither(): Either<E, A>
toList Convert this value to a single element List if it is Valid, otherwise return an empty Listfun toList(): List<A>
toOption Returns Valid values wrapped in Some, and None for Invalid valuesfun toOption(): Option<A>
toString open fun toString(): String
toValidatedNel Lift the Invalid value into a NonEmptyList.fun toValidatedNel(): ValidatedNel<E, A>
traverse fun <B> traverse(fa: (A) -> Iterable<B>): List<Validated<E, B>>
traverseEither fun <EE, B> traverseEither(fa: (A) -> Either<EE, B>): Either<EE, Validated<E, B>>
void Discards the A value inside Validated signaling this container may be pointing to a noop or an effect whose return value is deliberately ignored. The singleton value Unit serves as signal.fun void(): Validated<E, Unit>
withEither Convert to an Either, apply a function, convert back. This is handy when you want to use the Monadic properties of the Either type.fun <EE, B> withEither(f: (Either<E, A>) -> Either<EE, B>): Validated<EE, B>

Companion Object Functions

catch fun <A> catch(f: () -> A): Validated<Throwable, A>
suspend fun <A> ~~catch~~(f: suspend () -> A): Validated<Throwable, A>
fun <E, A> catch(recover: (Throwable) -> E, f: () -> A): Validated<E, A>
catchNel fun <A> catchNel(f: () -> A): ValidatedNel<Throwable, A>
suspend fun <A> ~~catchNel~~(f: suspend () -> A): ValidatedNel<Throwable, A>
fromEither Converts an Either<E, A> to a Validated<E, A>.fun <E, A> fromEither(e: Either<E, A>): Validated<E, A>
fromNullable Converts a nullable A? to a Validated<E, A>, where the provided ifNull output value is returned as Invalid when the specified value is null.fun <E, A> fromNullable(value: A?, ifNull: () -> E): Validated<E, A>
fromOption Converts an Option<A> to a Validated<E, A>, where the provided ifNone output value is returned as Invalid when the specified Option is None.fun <E, A> fromOption(o: Option<A>, ifNone: () -> E): Validated<E, A>
invalidNel fun <E, A> invalidNel(e: E): ValidatedNel<E, A>
lift Lifts a function A -> B to the Validated structure.fun <E, A, B> lift(f: (A) -> B): (Validated<E, A>) -> Validated<E, B>
Lifts two functions to the Bifunctor type.fun <A, B, C, D> lift(fl: (A) -> C, fr: (B) -> D): (Validated<A, B>) -> Validated<C, D>
validNel fun <E, A> validNel(a: A): ValidatedNel<E, A>

Extension Functions

altFold fun <T, F, A> Kind<T, A>.altFold(AF: Alternative<F>, FT: Foldable<T>): Kind<F, A>
altSum fun <T, F, A> Kind<T, Kind<F, A>>.altSum(AF: Alternative<F>, FT: Foldable<T>): Kind<F, A>
ap fun <E, A, B> ValidatedOf<E, A>.~~ap~~(SE: Semigroup<E>, f: Validated<E, (A) -> B>): Validated<E, B>
attempt fun <E, A> Validated<E, A>.attempt(): Validated<Nothing, Either<E, A>>
bisequence fun <E, A> Validated<Iterable<E>, Iterable<A>>.bisequence(): List<Validated<E, A>>
bisequenceEither fun <E, A, B> Validated<Either<E, A>, Either<E, B>>.bisequenceEither(): Either<E, Validated<A, B>>
combine fun <E, A> ValidatedOf<E, A>.combine(SE: Semigroup<E>, SA: Semigroup<A>, y: ValidatedOf<E, A>): Validated<E, A>
combineAll fun <E, A> Validated<E, A>.combineAll(MA: Monoid<A>): A
combineK fun <E, A> ValidatedOf<E, A>.combineK(SE: Semigroup<E>, y: ValidatedOf<E, A>): Validated<E, A>
compareTo operator fun <E : Comparable<E>, A : Comparable<A>> Validated<E, A>.compareTo(other: Validated<E, A>): Int
conest fun <F, A, B> CounnestedType<F, A, B>.conest(): ConestedType<F, A, B>
findValid If this is valid return this, otherwise if that is valid return that, otherwise combine the failures. This is similar to orElse except that here failures are accumulated.fun <E, A> ValidatedOf<E, A>.findValid(SE: Semigroup<E>, that: () -> Validated<E, A>): Validated<E, A>
fix fun <E, A> ValidatedOf<E, A>.~~fix~~(): Validated<E, A>
fold fun <E, A> Validated<E, A>.fold(MA: Monoid<A>): A
getOrElse Return the Valid value, or the default if Invalidfun <E, A> ValidatedOf<E, A>.getOrElse(default: () -> A): A
handleError fun <E, A> ValidatedOf<E, A>.handleError(f: (E) -> A): Validated<Nothing, A>
handleErrorWith fun <E, A> ValidatedOf<E, A>.handleErrorWith(f: (E) -> ValidatedOf<E, A>): Validated<E, A>
handleLeftWith fun <E, A> ValidatedOf<E, A>.~~handleLeftWith~~(f: (E) -> ValidatedOf<E, A>): Validated<E, A>
leftWiden fun <EE, E : EE, A> Validated<E, A>.leftWiden(): Validated<EE, A>
orElse Return this if it is Valid, or else fall back to the given default. The functionality is similar to that of findValid except for failure accumulation, where here only the error on the right is preserved and the error on the left is ignored.fun <E, A> ValidatedOf<E, A>.orElse(default: () -> Validated<E, A>): Validated<E, A>
orNull Return the Valid value, or null if Invalidfun <E, A> ValidatedOf<E, A>.orNull(): A?
redeem fun <E, A, B> Validated<E, A>.redeem(fe: (E) -> B, fa: (A) -> B): Validated<E, B>
replicate fun <E, A> Validated<E, A>.replicate(SE: Semigroup<E>, n: Int): Validated<E, List<A>>
fun <E, A> Validated<E, A>.replicate(SE: Semigroup<E>, n: Int, MA: Monoid<A>): Validated<E, A>
sequence fun <E, A> Validated<E, Iterable<A>>.sequence(): List<Validated<E, A>>
fun <G, E, A> ValidatedOf<E, Kind<G, A>>.~~sequence~~(GA: Applicative<G>): Kind<G, Validated<E, A>>
sequenceEither fun <E, A, B> Validated<A, Either<E, B>>.sequenceEither(): Either<E, Validated<A, B>>
toIor Converts the value to an Ior<E, A>fun <E, A> ValidatedOf<E, A>.toIor(): Ior<E, A>
traverse fun <G, E, A, B> ValidatedOf<E, A>.~~traverse~~(GA: Applicative<G>, f: (A) -> Kind<G, B>): Kind<G, Validated<E, B>>
valueOr Return the Valid value, or the result of f if Invalidfun <E, A> ValidatedOf<E, A>.valueOr(f: (E) -> A): A
widen Given A is a sub type of B, re-type this value from Validated<E, A> to Validated<E, B>fun <E, B, A : B> Validated<E, A>.widen(): Validated<E, B>
zip fun <E, A, B> Validated<E, A>.zip(SE: Semigroup<E>, fb: Validated<E, B>): Validated<E, Pair<A, B>>
fun <E, A, B, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, f: (A, B) -> Z): Validated<E, Z>
fun <E, A, B, C, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, c: Validated<E, C>, f: (A, B, C) -> Z): Validated<E, Z>
fun <E, A, B, C, D, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, c: Validated<E, C>, d: Validated<E, D>, f: (A, B, C, D) -> Z): Validated<E, Z>
fun <E, A, B, C, D, EE, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, c: Validated<E, C>, d: Validated<E, D>, e: Validated<E, EE>, f: (A, B, C, D, EE) -> Z): Validated<E, Z>
fun <E, A, B, C, D, EE, FF, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, c: Validated<E, C>, d: Validated<E, D>, e: Validated<E, EE>, ff: Validated<E, FF>, f: (A, B, C, D, EE, FF) -> Z): Validated<E, Z>
fun <E, A, B, C, D, EE, F, G, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, c: Validated<E, C>, d: Validated<E, D>, e: Validated<E, EE>, ff: Validated<E, F>, g: Validated<E, G>, f: (A, B, C, D, EE, F, G) -> Z): Validated<E, Z>
fun <E, A, B, C, D, EE, F, G, H, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, c: Validated<E, C>, d: Validated<E, D>, e: Validated<E, EE>, ff: Validated<E, F>, g: Validated<E, G>, h: Validated<E, H>, f: (A, B, C, D, EE, F, G, H) -> Z): Validated<E, Z>
fun <E, A, B, C, D, EE, F, G, H, I, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, c: Validated<E, C>, d: Validated<E, D>, e: Validated<E, EE>, ff: Validated<E, F>, g: Validated<E, G>, h: Validated<E, H>, i: Validated<E, I>, f: (A, B, C, D, EE, F, G, H, I) -> Z): Validated<E, Z>
fun <E, A, B, C, D, EE, F, G, H, I, J, Z> Validated<E, A>.zip(SE: Semigroup<E>, b: Validated<E, B>, c: Validated<E, C>, d: Validated<E, D>, e: Validated<E, EE>, ff: Validated<E, F>, g: Validated<E, G>, h: Validated<E, H>, i: Validated<E, I>, j: Validated<E, J>, f: (A, B, C, D, EE, F, G, H, I, J) -> Z): Validated<E, Z>
fun <E, A, B, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, f: (A, B) -> Z): ValidatedNel<E, Z>
fun <E, A, B, C, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, c: ValidatedNel<E, C>, f: (A, B, C) -> Z): ValidatedNel<E, Z>
fun <E, A, B, C, D, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, c: ValidatedNel<E, C>, d: ValidatedNel<E, D>, f: (A, B, C, D) -> Z): ValidatedNel<E, Z>
fun <E, A, B, C, D, EE, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, c: ValidatedNel<E, C>, d: ValidatedNel<E, D>, e: ValidatedNel<E, EE>, f: (A, B, C, D, EE) -> Z): ValidatedNel<E, Z>
fun <E, A, B, C, D, EE, FF, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, c: ValidatedNel<E, C>, d: ValidatedNel<E, D>, e: ValidatedNel<E, EE>, ff: ValidatedNel<E, FF>, f: (A, B, C, D, EE, FF) -> Z): ValidatedNel<E, Z>
fun <E, A, B, C, D, EE, F, G, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, c: ValidatedNel<E, C>, d: ValidatedNel<E, D>, e: ValidatedNel<E, EE>, ff: ValidatedNel<E, F>, g: ValidatedNel<E, G>, f: (A, B, C, D, EE, F, G) -> Z): ValidatedNel<E, Z>
fun <E, A, B, C, D, EE, F, G, H, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, c: ValidatedNel<E, C>, d: ValidatedNel<E, D>, e: ValidatedNel<E, EE>, ff: ValidatedNel<E, F>, g: ValidatedNel<E, G>, h: ValidatedNel<E, H>, f: (A, B, C, D, EE, F, G, H) -> Z): ValidatedNel<E, Z>
fun <E, A, B, C, D, EE, F, G, H, I, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, c: ValidatedNel<E, C>, d: ValidatedNel<E, D>, e: ValidatedNel<E, EE>, ff: ValidatedNel<E, F>, g: ValidatedNel<E, G>, h: ValidatedNel<E, H>, i: ValidatedNel<E, I>, f: (A, B, C, D, EE, F, G, H, I) -> Z): ValidatedNel<E, Z>
fun <E, A, B, C, D, EE, F, G, H, I, J, Z> ValidatedNel<E, A>.zip(b: ValidatedNel<E, B>, c: ValidatedNel<E, C>, d: ValidatedNel<E, D>, e: ValidatedNel<E, EE>, ff: ValidatedNel<E, F>, g: ValidatedNel<E, G>, h: ValidatedNel<E, H>, i: ValidatedNel<E, I>, j: ValidatedNel<E, J>, f: (A, B, C, D, EE, F, G, H, I, J) -> Z): ValidatedNel<E, Z>

Do you like Arrow?

Arrow Org
<