i’m trying to understand how to design the followi...
# arrow
d
i’m trying to understand how to design the following with validated — i have a nullable field. if it is
null
, i want to return an error. if not, i want to run several validations against the non-null data. how would i go about this?
.withEither
doesn’t seem correct, because i want to operate with
Validated
, not
Either
.
r
Hi @drew, maybe something like this
Note
A
there is not constrained to
Any
so it really works on nullable types too. If you want something more concrete and expicitly target nullable types you may want to have an error ADT where NotFound is an option
Alternatively if you don’t want the validated api to show up in the validation rules you can also use something lie this which depends on a map of string to boolean predicates for the rules:
d
how would this work if
1
is instead
null
r
you would have to deal with null in the right in that case but if you want to explicitly validate null you may want to constrain E to something like:
Copy code
sealed class Errors
object NullOrNotFound : Errors()
...
d
basically i want a slightly better way to do this:
r
so that
E: Errors
d
Copy code
fun validateAge(age: Int?): Validated<PersonError, Int> =
    age?.let {
        if (it < 11) {
            Invalid(IsTooSmall(10))
        } else {
            Valid(it)
        }
    } ?: Invalid(IsNull("age"))
and in general, the
let
could actually be
zip
ing several validations
r
in general validation is not mixed with null or represented how you have it there. To safely handle the null case you need a case in the left or have the right be nullable in which case you would return Valid(null) and handle it afterward
There is nothing validation can do to erase the nullability effect unless you tell it how it is represented in the left or the right
d
yep, got it
and there’s also no concept of “chaining” as it is an applicative based API, right?
r
Well you can do it better in Arrow since the Either effect is more than comprehensions and can also provide monad bind for Validation even though validated is not a monad by implicitly turning it into either
it may look like this:
Copy code
either {
  val result = validationValue.bind()
}
then there is functions to go back and forth between either and validated
you can intermix there binding with suspension too, this is actually more like an EitherT transformer over IO since inline functions defined with these apis can be used both in suspend and regular functions
d
got it, thanks
👍 1
i ended up with something like this:
Copy code
import arrow.core.*
import arrow.core.computations.either

sealed class PersonError(message: String) {
    data class IsNull(val field: String): PersonError("$field is null")
    data class IsTooSmall(val limit: Int): PersonError("must be larger than $limit")
}

data class Person(val name: String, val age: Int)

typealias IsNull = PersonError.IsNull
typealias IsTooSmall = PersonError.IsTooSmall

fun <A>validateNotNull(value: A?, field: String): Validated<PersonError, A> =
    value?.let(::Valid) ?: Invalid(IsNull(field))

fun validateName(name: String?): Validated<PersonError, String> =
    validateNotNull(name, "name")

fun validateAgeFields(age: Int): ValidatedNel<PersonError, Int> =
    if (age < 10) {
        Invalid(IsTooSmall(10))
    } else {
        Valid(age)
    }.toValidatedNel()

suspend fun validateAge(age: Int?): ValidatedNel<PersonError, Int> =
    Validated.fromEither(either {
        val goodAge = validateNotNull(age, "age").toValidatedNel().bind()
        validateAgeFields(goodAge).bind()
    })

suspend fun validatePerson(name: String?, age: Int?): ValidatedNel<PersonError, Person> =
    validateName(name)
        .toValidatedNel()
        .zip(validateAge(age))
        { goodName, goodAge -> Person(goodName, goodAge) }
r
LGTM!