Share
Embed
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
Transcript
18 slides
-
Requirements 1. Turn on TV 2. Watch the movie 3.
Write the review of the movie -
Model data class TV data class MovieOpinion data class Review
fun turnOn(): TV = TODO() fun watch(): MovieOpinion = TODO() fun writeReview(): Review = TODO() -
Exceptions fun turnOn(): TV = throw new RuntimeException("RemoteNotFound") fun watch():
MovieOpinion = throw new NotInNetflixException("NotInNetflix") fun writeReview(): Review = Review() -
Issues with Exceptions 1. Breaks referential transparency 2. Broken GOTO
3. Try catch blocks 4. Costly to construct -
Poor choice for - Modelling absence - Known alternate paths
- Async - No access to source code -
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 -
Option fun turnOn(): Option<TV> = none() fun watch(): Option<MovieOpinion> =
some(MovieOpinion()) fun writeReview(): Review? = null -
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() -
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") }} ) -
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) -
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 -
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=@))) -
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))) ))*/ -
TLDR - Modelling absence -> Option / Nullable - Known
alternate paths -> Either - Validations -> Either/Validated