Skip to content

Instantly share code, notes, and snippets.

@vihangpatil
Last active July 27, 2022 09:07
Show Gist options
  • Save vihangpatil/0e11819ef92ffe2298edeaf476737901 to your computer and use it in GitHub Desktop.
Save vihangpatil/0e11819ef92ffe2298edeaf476737901 to your computer and use it in GitHub Desktop.
Kotlin custom monad bind() for Arrow-kt Validated returning ValidatedNei similar to either, option and nullable.
import arrow.core.NonEmptyList
import arrow.core.Validated
import arrow.core.ValidatedNel
import arrow.core.invalid
import arrow.core.nonEmptyListOf
import arrow.core.valid
interface ValidatedEffect<INVALID, VALID> {
suspend fun toValidatedNel(): ValidatedNel<INVALID, VALID>
}
interface ValidatedEffectScope<INVALID, VALID> {
suspend fun shift(invalid: INVALID)
suspend fun Validated<INVALID, VALID>.bind(): Validated<INVALID, VALID> {
when (this) {
is Validated.Valid -> value
is Validated.Invalid -> shift(value)
}
return this
}
}
@Suppress("ClassName")
object validated {
suspend inline operator fun <INVALID, VALID> invoke(
crossinline f: suspend ValidatedEffectScope<INVALID, VALID>.() -> VALID
): ValidatedNel<INVALID, VALID> = validatedEffect(f).toValidatedNel()
}
inline fun <INVALID, VALID> validatedEffect(
crossinline f: suspend ValidatedEffectScope<INVALID, VALID>.() -> VALID
): ValidatedEffect<INVALID, VALID> = object : ValidatedEffect<INVALID, VALID> {
override suspend fun toValidatedNel(): ValidatedNel<INVALID, VALID> {
var invalidList: NonEmptyList<INVALID>? = null
val effectScope = object : ValidatedEffectScope<INVALID, VALID> {
override suspend fun shift(invalid: INVALID) {
invalidList = invalidList?.let { it + invalid } ?: nonEmptyListOf(invalid)
}
}
val valid = f(effectScope)
return invalidList?.invalid() ?: valid.valid()
}
}
import arrow.core.NonEmptyList
import arrow.core.Validated
import arrow.core.ValidatedNel
import arrow.core.invalid
import arrow.core.nonEmptyListOf
import arrow.core.valid
import kotlinx.coroutines.runBlocking
import java.util.*
data class Identity(
val name: String,
val age: Int,
val countryCode: String,
)
sealed class InvalidIdentity(
val errorMessage: String
) {
object BlankName : InvalidIdentity("name is blank")
class LowAge(age: Int) : InvalidIdentity("age($age) < 18")
object BlankCountryCode : InvalidIdentity("country code is blank")
class CountryCodeNotFound(code: String) : InvalidIdentity("country code($code) not found")
}
suspend fun validate(
identity: Identity
): ValidatedNel<InvalidIdentity, Identity> {
return validated {
if (identity.age < 18) {
InvalidIdentity.LowAge(identity.age).invalid().bind()
}
if (identity.countryCode.isBlank()) {
InvalidIdentity.BlankCountryCode.invalid().bind()
}
if (!Locale.getISOCountries(Locale.IsoCountryCode.PART1_ALPHA3).contains(identity.countryCode.uppercase())) {
InvalidIdentity.CountryCodeNotFound(identity.countryCode).invalid().bind()
}
if (!Locale.getISOCountries(Locale.IsoCountryCode.PART1_ALPHA3).contains(identity.countryCode.uppercase())) {
InvalidIdentity.CountryCodeNotFound(identity.countryCode).invalid().bind()
}
if (identity.name.isBlank()) {
InvalidIdentity.BlankName.invalid().bind()
}
identity
}
}
fun main() {
runBlocking {
validate(
Identity(
name = "",
age = 0,
countryCode = ""
)
).fold(
{ errors -> errors.forEach { error -> println("error: ${error.errorMessage}") } },
{ identity -> println("valid: $identity") }
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment