Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Error Handling With ArrowKT

Functional Error Handling With ArrowKT

Functional error handling with Kotlin
Abolishing try-catch blocks and making your code more readable using functional programming

Uriel Salischiker

May 26, 2021
Tweet

More Decks by Uriel Salischiker

Other Decks in Programming

Transcript

  1. Requirements 1. Turn on TV 2. Watch the movie 3.

    Write the review of the movie
  2. Model data class TV data class MovieOpinion data class Review

    fun turnOn(): TV = TODO() fun watch(): MovieOpinion = TODO() fun writeReview(): Review = TODO()
  3. Exceptions fun turnOn(): TV = throw new RuntimeException("RemoteNotFound") fun watch():

    MovieOpinion = throw new NotInNetflixException("NotInNetflix") fun writeReview(): Review = Review()
  4. OK for - Fatal errors that you can’t recover from

    - Contributor to JVM internals - Want to create chaos and overthrow the government - This talk - You know what you are doing
  5. Option fun turnOn(): Option<TV> = none() fun watch(): Option<MovieOpinion> =

    some(MovieOpinion()) fun writeReview(): Review? = null
  6. Either sealed class ReviewError object RemoteNotFound: ReviewError object NotInNetflix: ReviewError

    fun turnOn(): Either<RemoteNotFound, TV> = RemoteNotFound.left() fun watch(): Either<NotInNetflix, MovieOpinion> = MovieOpinion.right() fun writeReview(): Review = Review()
  7. Either suspend fun writeReview() = either { val tv =

    turnOn().bind() val opinion = watch().bind() writeActualReview(opinion) } writeReview().fold( { println("Review: $it") }, { when(it) { RemoteNotFound -> println("Remote") NotInNetflix -> println("Netflix") }} )
  8. Validated sealed class ValidationError(val msg: String) { data class DoesNotContain(val

    value: String) : ValidationError("Did not contain $value") data class MaxLength(val value: Int) : ValidationError("Exceeded length of $value") data class NotAnEmail(val reasons: Nel<ValidationError>) : ValidationError("Not a valid email") } data class FormField(val label: String, val value: String) data class Email(val value: String)
  9. Validated sealed class Strategy { object FailFast : Strategy() object

    ErrorAccumulation : Strategy() } /** Abstracts away invoke strategy **/ object Rules { private fun FormField.contains(needle: String): ValidatedNel<ValidationError, FormField> = if (value.contains(needle, false)) validNel() else ValidationError.DoesNotContain(needle).invalidNel() private fun FormField.maxLength(maxLength: Int): ValidatedNel<ValidationError, FormField> = if (value.length <= maxLength) validNel() else ValidationError.MaxLength(maxLength).invalidNel() private fun FormField.validateErrorAccumulate(): ValidatedNel<ValidationError, Email> = contains("@").zip( Semigroup.nonEmptyList(), // accumulates errors in a non empty list, can be omited for NonEmptyList maxLength(250) ) { _, _ -> Email(value) }.handleErrorWith { ValidationError.NotAnEmail(it).invalidNel() } /** either blocks support binding over Validated values with no additional cost or need to convert first to Either **/ private fun FormField.validateFailFast(): Either<Nel<ValidationError>, Email> = either.eager { contains("@").bind() // fails fast on first error found maxLength(250).bind() Email(value) } operator fun invoke(strategy: Strategy, fields: List<FormField>): Either<Nel<ValidationError>, List<Email>> = when (strategy) { Strategy.FailFast -> fields.traverseEither { it.validateFailFast() } Strategy.ErrorAccumulation -> fields.traverseValidated(Semigroup.nonEmptyList()) { it.validateErrorAccumulate() }.toEither() } } https://arrow-kt.io/docs/patterns/error_handling/ Boilerplate
  10. Validated val fields = listOf( FormField("Invalid Email Domain Label", "nowhere.com"),

    //this fails FormField("Too Long Email Label", "nowheretoolong${(0..251).map { "g" }}") FormField("Valid Email Label", "[email protected]") ) Rules(Strategy.FailFast, fields) // Either.Left(NonEmptyList(DoesNotContain(value=@)))
  11. Validated val fields = listOf( FormField("Invalid Email Domain Label", "nowhere.com"),

    //this fails FormField("Too Long Email Label", "nowheretoolong${(0..251).map { "g" }}") FormField("Valid Email Label", "[email protected]") ) Rules(Strategy.ErrorAccumulation, fields) /* Either.Left(NonEmptyList( NotAnEmail(reasons=NonEmptyList(DoesNotContain(value=@))), NotAnEmail(reasons=NonEmptyList(DoesNotContain(value=@), MaxLength(value=250))) ))*/
  12. TLDR - Modelling absence -> Option / Nullable - Known

    alternate paths -> Either - Validations -> Either/Validated