Share
Embed
Async and Concurrent Error Handling & Cancellation
All programs contain side effects. They load data from network, persist to a database, log to console, and display information on screen. In this talk, we will learn how side effects break determinism, and how that smashes our ability to reason about our programs.
Then, we will move onto how we can keep those effects under control using the powers of IO, and how to use the DSL ArrowFx provides to write any pure functional program with a simple syntax.
This presentation was given by Jorge Castillo as part of the 47 Degrees Academy. You can watch the talk in full here: https://www.youtube.com/watch?v=UkcEXUlycC8
47 Degrees Academy
June 11, 2020
Transcript
58 slides
-
Our program - chain ops Target platform: JVM Running on Arrow v. 0.10.5 Running on Kotlin v. 1.3.50 fun loadTalks(ids: List<TalkId>): Either<InvalidSpeakerId, List<Talk>> = listOf( Talk(45, "Some talk script"), Talk(50, "Another talk script")).right() fun main() { val talks = loadSpeaker("SomeId")
.flatMap { loadTalks(it.talkIds) } println(talks) } -
Fail fast First operation fails short circuit Target platform: JVM Running on Arrow v. 0.10.5 Running on Kotlin v. 1.3.50 fun main() { val events = Either.fx<Error, List<Event>> { val speaker = !loadSpeaker("SomeId") val talks = !loadTalks(speaker.talkIds)
val events = talks.map { !loadEvent(it.event) } events } println(events) } -
Validated & ValidatedNel Target platform: JVM Running on Arrow v. 0.10.5 Running on Kotlin v. 1.3.50 fun validateTalk(talk: Talk): ValidatedNel<Error, Talk> = if (talk.script.length < 25) { Error.ScriptTooShort.invalidNel() } else if (talk.script.contains("ugly words")) { Error.ViolatesCodeOfConduct.invalidNel() } else if (talk.duration > 45) {
Error.TalkTooLong.invalidNel() } else { talk.validNel() } val res1 = validateTalk(Talk(45, "Some too short script", "EventId1")) val res2 = validateTalk(Talk(45, "This is a long enough script but with ugly words", "EventId2")) val res3 = validateTalk(Talk(60, "This is a long enough script", "EventId3")) -
Either within executor thread val speakerService = SpeakerService() val executor = Executors.newFixedThreadPool(4) val callback: (Either<Error, Speaker>) -> Unit = { either -> either.fold( ifLeft = { error -> println(error) }, ifRight = { speaker -> println(speaker) } ) } executor.execute {
val speaker = speakerService.loadSpeaker("SomeId") callback(speaker) } -
Either within stdlib coroutine Better, but cancellation? KotlinX Coroutines required, or an alternative runtime library. Target platform: JVM Running on Arrow v. 0.10.5 Running on Kotlin v. 1.3.50 suspend fun main() { // kotlin suspend main val speakerService = SpeakerService() val speaker = speakerService.loadSpeaker("SomeId")
speaker.fold( ifLeft = { println(it) }, ifRight = { println(it) } ) } -
Either within IO disposable() for cancellation. Target platform: JVM Running on Arrow v. 0.10.5 Running on Kotlin v. 1.3.50 fun main() { val speakerService = SpeakerService() val speaker = IO { speakerService.loadSpeaker("SomeId") } val disposable = speaker.unsafeRunAsyncCancellable { it.fold(
ifLeft = { error -> println(error) }, ifRight = { speaker -> println(speaker) } ) } } -
Why not IO<A>? Only supports Throwable errors. No strongly typed errors. sealed class Error : Throwable() { object SpeakerNotFound : Error() object InvalidSpeakerId : Error() object InvalidEventId : Error() } IO { speakerService.loadSpeaker("SomeId") } .handleError {
when (it as Error) { // unsafe casting SpeakerNotFound -> TODO() InvalidSpeakerId -> TODO() Error.InvalidEventId -> TODO() } } -
IO<Either<A, B>> suspend () -> Either<A, B> A program written for IO. suspend fun fetchUser(): Either<PersistenceError, User> = TODO() suspend fun User.process(): Either<PersistenceError, ProcessedUser> = TODO() fun program(): IO<Either<PersistenceError, ProcessedUser>> = IO.fx { val user = !IO.effect { fetchUser() } !user.fold({ error ->
IO.just(Either.Left(error)) }, { user -> IO.effect { user.process() } }) } -
Guarantees stay IO#map and IO#flatMap not needed. suspend fun program(): Either<PersistenceError, ProcessedUser> = Either.fx { val user = !fetchUser() val processed = !user.process() processed } // or suspend fun program(): Either<PersistenceError, ProcessedUser> =
fetchUser().flatMap { it.process() } -
parMapN Run N parallel ops. Cancelling parent cancels all children. Child failure cancels other children. suspend fun main() { val op1 = suspend { loadSpeakers() } val op2 = suspend { loadRooms() } val op3 = suspend { loadVenues() }
val program = parMapN(op1, op2, op3) { (speakers, rooms, venues) -> Event(speakers, rooms, venues) } program.cancel() // cancels all child ops. } -
parTupledN Same without callback style. suspend computation returns a tuple of all the results. suspend fun main() { val op1 = suspend { loadSpeakers() } val op2 = suspend { loadRooms() } val op3 = suspend { loadVenues() } val res: Triple<List<Speakers>, List<Rooms>, List<Venues>> =
parTupledN(op1, op2, op3) Event(res.first, res.second, res.third) } -
parTraverse Traverse a series of elements of the same type parallelly. Applies a mapping function for each. Cancelling parent cancels all children. Child failure cancels other children. suspend fun loadEvents() { val eventIds = listOf(1, 2, 3)
return eventIds.parTraverse(IOPool) { id -> eventService.loadEvent(id) } } -
parSequence Same thing without the mapping function. Cancelling parent cancels all children. Child failure cancels other children. suspend fun main() { val ops = listOf( suspend { service.loadTalks(eventId1) }, suspend { service.loadTalks(eventId2) },
suspend { service.loadTalks(eventId3) }) ops.parSequence() } -
raceN Racing computations parallelly. Returns the winner, cancels losers. Cancelling parent cancels all children. Child failure cancels other children. suspend fun main() { val op1 = suspend { loadSpeakers() } val op2 = suspend { loadRooms() }
val op3 = suspend { loadVenues() } val res = raceN(op1, op2, op3) res.fold( ifA = {}, ifB = {}, ifC = {} ) } -
Error handling? suspend fun getTalks() { val ops = listOf( suspend { service.loadTalks(eventId1) }, suspend { service.loadTalks(eventId2) }, suspend { service.loadTalks(eventId3) }) ops.parSequence() }
suspend fun main() { val res = Either.catch { getTalks() } res.fold( ifLeft = { /*...*/ }, ifRight = { /*...*/ } )