Slide 1

Slide 1 text

Functional error handling with Arrow Uriel Salischiker - urielsalis.com

Slide 2

Slide 2 text

Requirements 1. Turn on TV 2. Watch the movie 3. Write the review of the movie

Slide 3

Slide 3 text

Model data class TV data class MovieOpinion data class Review fun turnOn(): TV = TODO() fun watch(): MovieOpinion = TODO() fun writeReview(): Review = TODO()

Slide 4

Slide 4 text

Exceptions fun turnOn(): TV = throw new RuntimeException("RemoteNotFound") fun watch(): MovieOpinion = throw new NotInNetflixException("NotInNetflix") fun writeReview(): Review = Review()

Slide 5

Slide 5 text

Issues with Exceptions 1. Breaks referential transparency 2. Broken GOTO 3. Try catch blocks 4. Costly to construct

Slide 6

Slide 6 text

Poor choice for - Modelling absence - Known alternate paths - Async - No access to source code

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

So how do we model them?

Slide 9

Slide 9 text

ARROW

Slide 10

Slide 10 text

Option fun turnOn(): Option = none() fun watch(): Option = some(MovieOpinion()) fun writeReview(): Review? = null

Slide 11

Slide 11 text

Either sealed class ReviewError object RemoteNotFound: ReviewError object NotInNetflix: ReviewError fun turnOn(): Either = RemoteNotFound.left() fun watch(): Either = MovieOpinion.right() fun writeReview(): Review = Review()

Slide 12

Slide 12 text

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") }} )

Slide 13

Slide 13 text

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("Not a valid email") } data class FormField(val label: String, val value: String) data class Email(val value: String)

Slide 14

Slide 14 text

Validated sealed class Strategy { object FailFast : Strategy() object ErrorAccumulation : Strategy() } /** Abstracts away invoke strategy **/ object Rules { private fun FormField.contains(needle: String): ValidatedNel = if (value.contains(needle, false)) validNel() else ValidationError.DoesNotContain(needle).invalidNel() private fun FormField.maxLength(maxLength: Int): ValidatedNel = if (value.length <= maxLength) validNel() else ValidationError.MaxLength(maxLength).invalidNel() private fun FormField.validateErrorAccumulate(): ValidatedNel = 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, Email> = either.eager { contains("@").bind() // fails fast on first error found maxLength(250).bind() Email(value) } operator fun invoke(strategy: Strategy, fields: List): Either, List> = 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

Slide 15

Slide 15 text

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=@)))

Slide 16

Slide 16 text

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))) ))*/

Slide 17

Slide 17 text

TLDR - Modelling absence -> Option / Nullable - Known alternate paths -> Either - Validations -> Either/Validated

Slide 18

Slide 18 text

Questions?