Extending vavr with an eitherblock for better functional programming

Ik ben een groot voorstanden om het Either type te gebruiken in plaats van Excepties (zie Error handling: Excepties vs Either)

Aangezien Kotlin helaas zelf geen support heeft voor Either, ben je afhankelijk van een externe library (of je moet hem zelf implementeren). 2 veel gebruikte libraries voor functioneel programmeren in Kotlin zijn vavr en arrow.

Het nadeel van Either vind ik echter dat sommige blokken code lastiger te lezen zijn.

Dit is bijvoorbeeld een block code met Excepties:

@PostMapping("/brief1/{klantid}")
private fun briefController(@PathVariable klantid: Int, @RequestBody body: String): ResponseEntity<String?> {
    return try {
        val klant: Klant = zoekKlant(klantid).orElseThrow { KlantException("Klant niet gevonden") }
        val isZakelijkeKlant: Boolean = isZakelijkeKlant(klant)
        val adres: Adres = findAdres(klant, isZakelijkeKlant).orElseThrow { AdresException("Adres niet gevonden") }
        val brief: Brief = genereerBrief(adres, klant, body)
        val verstuurResult: VerstuurResult = verstuurBrief(brief)
        val result: String = verstuurResult.result
        ResponseEntity.ok(result)
    } catch (ex: KlantException) {
        ResponseEntity.status(400).body(ex.message)
    } catch (ex: AdresException) {
        ResponseEntity.status(400).body(ex.message)
    } catch (ex: BriefException) {
        ResponseEntity.status(400).body(ex.message)
    } catch (ex: SendException) {
        ResponseEntity.status(400).body(ex.message)
    }
}

Als we dit block willen herschrijven naar een functionele stijl met het gebruik van Either ipv Excepties, dan wordt de code alsvolgd:

@PostMapping("/brief2/{klantid}\"")
private fun briefController(@PathVariable klantid: Int, @RequestBody body: String): ResponseEntity<String> {
    return zoekKlant(klantid).flatMap { klant ->
        val isZakelijkeKlant: Either<String, Klant> = isZakelijkeKlant(klant)
        val eitherAdres: Either<String, Adres> = findAdres (isZakelijkeKlant, klant)
        val verstuurResult: Either<String, VerstuurResult> = eitherAdres.flatMap { adres: Adres ->
            val briefEither: Either<String, Brief> = genereerBrief(adres, klant, body)
            briefEither.flatMap { brief -> verstuurBrief(brief) }
        }
        verstuurResult
    }.fold(
            { err: String -> ResponseEntity.status(400).body(err) },
            { result: VerstuurResult -> ResponseEntity.ok(result.result) }
    )
}




Dit is nu wel funcioneel, maar toch wel minder leesbaar. Dit komt omdat op elke Either een map of flatmap gedaan moet worden.

Met de arrow fx library kun je dit herschrijven naar weer een leesbare stuk code:

@PostMapping("/brief3/{klantid}\"")
private fun briefController(@PathVariable klantid: Int, @RequestBody body: String): ResponseEntity<String> {
    return Either.fx<String, VerstuurResult> {
        val klant = zoekKlant(klantid).bind()
        val zakelijkeKlant = isZakelijkeKlant(klant)
        val adres = findAdres(zakelijkeKlant, klant).bind()
        val brief = genereerBrief(adres, klant, body).bind()
        val result = verstuurBrief(brief).bind()
        result
    }.fold(
            { err -> ResponseEntity.status(400).body(err) },
            { result -> ResponseEntity.ok(result.result) }
    )
} 

De truuk in deze code is het Either.fx block (uit de arrow-fx library) en de .bind() functie die op de either’s aangeroepen wordt.
De .bind() zorgt er voor dat er ofwel de either.right terug gegeven wordt, of dat het gehele either block de either.left teruggeeft.

Hierdoor kun je binnen het Either.fx block toch nog op een leesbare manier de logica programmeren.

Persoonlijk vind ik vavr echter iets fijner werken dan arrow, maar die mist wel deze functionaliteit.

Dit is echter zelf na te maken door de volgende code toe te voegen:

fun <A, B> eitherBlock(c: suspend () -> B): Either<A, B> =
        try {
            Either.right(runBlocking { c.invoke() })
        } catch (e: EitherBlockException) {
            Either.left(e.left as A)
        }
suspend fun <A, B> Either<A, B>.bind(): B = this.getOrElseThrow { _ -> EitherBlockException(this.left as Any) }  // suspend, om te forceren dat hij alleen maar binnen de eitherBlock draait
class EitherBlockException(val left: Any) : Exception()

Daarna kun je het voorbeeld herschrijven naar het volgende:

@PostMapping("/brief4/{klantid}\"")
private fun briefController(@PathVariable id: Int, @RequestBody body: String): ResponseEntity<String> {
    return eitherBlock<String, VerstuurResult> {
        val klant = zoekKlant(id).bind()
        val zakelijkeKlant = isZakelijkeKlant(klant)
        val adres = findAdres(zakelijkeKlant, klant).bind()
        val brief = genereerBrief(adres, klant, body).bind()
        val verstuurResult = verstuurBrief(brief).bind()
        verstuurResult
    }.fold(
            { err -> ResponseEntity.status(400).body(err) },
            { result -> ResponseEntity.ok(result.result) }
    )
}

Deze code heeft nu het voordeel dat het wel functioneel is en Either gebruikt voor de fout situaties, maar toch nog goed leesbaar is zonder geneste maps en flatmaps.

De volledige sourcecode is te vinden op:

https://github.com/robbertvdzon/eithervsexceptions/blob/master/src/main/java/com/vdzon/monads/kotlin/BriefControllerMetVavrEitherBlock.kt