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

Functional Error Handling In Kotlin - A Practical Approach

84e9d61d307ee2f59e9a87c1990bb765?s=47 Bas
June 14, 2022

Functional Error Handling In Kotlin - A Practical Approach

84e9d61d307ee2f59e9a87c1990bb765?s=128

Bas

June 14, 2022
Tweet

Other Decks in Technology

Transcript

  1. Bas de Groot - June 2022 - Kotlin Meetup Amsterdam

    Functional Error Handling A practical approach
  2. @basdgrt @basdgrt Photo by Anton Atanasov on Unsplash

  3. @basdgrt

  4. @basdgrt

  5. @basdgrt

  6. @basdgrt

  7. @basdgrt Relative cost to fi x bugs Based on the

    time of detection 0 10 20 30 Coding Automated testing Acceptance testing Production
  8. @basdgrt Catching bugs

  9. @basdgrt Catching bugs Unit and integration tests

  10. @basdgrt Catching bugs Unit and integration tests Code reviews

  11. @basdgrt Catching bugs Unit and integration tests Code reviews Acceptance

    testing
  12. @basdgrt Catching bugs Unit and integration tests Code reviews Acceptance

    testing Logging and monitoring
  13. @basdgrt Compile time safety

  14. @basdgrt Compile time safety

  15. @basdgrt Compile time safety Compiler can guarantee certain errors are

    not present
  16. @basdgrt Compile time safety Compiler can guarantee certain errors are

    not present Pair programming with someone who never makes a mistake
  17. @basdgrt Compile time safety Compiler can guarantee certain errors are

    not present Pair programming with someone who never makes a mistake Kotlin offers some amazing features
  18. @basdgrt Bugs spotted at compile time

  19. @basdgrt Bugs spotted at compile time Are easy and cheap

    to fi x
  20. @basdgrt Bugs spotted at compile time Are easy and cheap

    to fi x Will never have any business impact
  21. @basdgrt Bugs spotted at compile time Are easy and cheap

    to fi x Will never have any business impact But… This only works if we design our code in the correct way
  22. @basdgrt Designing for compile time safety enum class Animal {

    CAT, DOG, LION }
  23. @basdgrt Designing for compile time safety enum class Animal {

    CAT, DOG, LION } fun isPet(animal: Animal): Boolean { if (animal == Animal.CAT || animal == Animal.DOG) { return true } return false }
  24. @basdgrt Designing for compile time safety enum class Animal {

    CAT, DOG, LION } fun isPet(animal: Animal): Boolean { if (animal == Animal.CAT || animal == Animal.DOG) { return true } return false } Add GOLDFISH
  25. @basdgrt Designing for compile time safety enum class Animal {

    CAT, DOG, LION } fun isPet(animal: Animal): Boolean { if (animal == Animal.CAT || animal == Animal.DOG) { return true } return false } fun isPet(animal: Animal) = when (animal) { Animal.CAT, Animal.DOG -> true Animal.LION -> false } Add GOLDFISH
  26. @basdgrt Designing for compile time safety enum class Animal {

    CAT, DOG, LION } fun isPet(animal: Animal): Boolean { if (animal == Animal.CAT || animal == Animal.DOG) { return true } return false } fun isPet(animal: Animal) = when (animal) { Animal.CAT, Animal.DOG -> true Animal.LION -> false } Add GOLDFISH Compile time error when new animal is added to our enum
  27. @basdgrt Bear with me

  28. @basdgrt Bear with me Try to see this example in

    a larger context
  29. @basdgrt Bear with me Try to see this example in

    a larger context Hope you fi nd all related logic fl ows
  30. @basdgrt Bear with me Try to see this example in

    a larger context Hope you fi nd all related logic fl ows Hope some unit test will fail
  31. @basdgrt Bear with me Try to see this example in

    a larger context Hope you fi nd all related logic fl ows Hope some unit test will fail Hope your coworker notices during code review
  32. @basdgrt My girlfriend “Hope is postponed disappointment”

  33. @basdgrt This was just the warming up

  34. @basdgrt This was just the warming up Simple but important

  35. @basdgrt This was just the warming up Simple but important

    Part 2 will be more exciting, but also more complex
  36. @basdgrt This was just the warming up Simple but important

    Part 2 will be more exciting, but also more complex Baby steps
  37. @basdgrt offeeshop

  38. @basdgrt Coffee machine class CoffeeMachine { fun makeCoffee(): Coffee {

    val beans = grindBeans() return brew(beans) } }
  39. @basdgrt Coffee machine class CoffeeMachine { fun makeCoffee(): Coffee {

    val beans = grindBeans() return brew(beans) } } fun grindBeans(): CoffeeBeans = TODO() fun brew(beans: CoffeeBeans): Coffee = TODO()
  40. @basdgrt Problem fun grindBeans(): CoffeeBeans

  41. @basdgrt Problem fun grindBeans(): CoffeeBeans Takes no arguments

  42. @basdgrt Problem fun grindBeans(): CoffeeBeans Takes no arguments Returns CoffeeBeans

  43. @basdgrt Problem fun grindBeans(): CoffeeBeans class Barista(private val machine: CoffeeMachine)

    { fun handleOrder() { val coffee = machine.makeCoffee() serveToCustomer(coffee) } } Takes no arguments Returns CoffeeBeans
  44. @basdgrt Problem fun grindBeans(): CoffeeBeans class Barista(private val machine: CoffeeMachine)

    { fun handleOrder() { val coffee = machine.makeCoffee() serveToCustomer(coffee) } } Takes no arguments Returns CoffeeBeans Uses grindBeans()
  45. @basdgrt Problem class Barista(private val machine: CoffeeMachine) { fun handleOrder()

    { val coffee = machine.makeCoffee() serveToCustomer(coffee) } } /** * @throws NotEnoughBeansException */ fun grindBeans(): CoffeeBeans Takes no arguments Returns CoffeeBeans Uses grindBeans()
  46. @basdgrt Problem /** * @throws NotEnoughBeansException */ fun grindBeans(): CoffeeBeans

    class Barista(private val machine: CoffeeMachine) { fun handleOrder() = try { val coffee = machine.makeCoffee() serveToCustomer(coffee) } catch (e: NotEnoughBeansException) { // Do something smart... } } Takes no arguments Returns CoffeeBeans Uses grindBeans()
  47. @basdgrt Problem /** * @throws NotEnoughBeansException * @throws NoPowerException */

    fun grindBeans(): CoffeeBeans class Barista(private val machine: CoffeeMachine) { fun handleOrder() = try { val coffee = machine.makeCoffee() serveToCustomer(coffee) } catch (e: NotEnoughBeansException) { // Do something smart... } } Takes no arguments Returns CoffeeBeans Uses grindBeans()
  48. @basdgrt Problem summary

  49. @basdgrt Problem summary Function signature is incorrect

  50. @basdgrt Problem summary Function signature is incorrect Compiler doesn’t force

    us to handle the unhappy path
  51. @basdgrt Problem summary Function signature is incorrect Compiler doesn’t force

    us to handle the unhappy path Can’t design for compile time safety
  52. @basdgrt So what’s the big deal?

  53. @basdgrt So what’s the big deal? Improper error handling gives

    runtime issues
  54. @basdgrt So what’s the big deal? Improper error handling gives

    runtime issues Runtime issues are expensive to fi x
  55. @basdgrt So what’s the big deal? Improper error handling gives

    runtime issues Runtime issues are expensive to fi x Readability suffers
  56. @basdgrt Monads

  57. @basdgrt Saunders Mac Lane - Categories for the Working Mathematician

    “A monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the identity endofunctor.”
  58. @basdgrt A monad is a functional design pattern

  59. @basdgrt A monad is a wrapper around a return value

  60. @basdgrt Different monad fl avours Nullability: Maybe/Option monad Iterable: List

    monad Logging: Writer monad Error handling: Either monad And many more…
  61. @basdgrt Either monad

  62. @basdgrt

  63. @basdgrt 😢

  64. @basdgrt 😢 😁

  65. @basdgrt 😢 😁 ✅ ❌

  66. @basdgrt 😢 😁 ✅ ❌ ➡ ⬅

  67. @basdgrt Coding up an Either sealed class Either<out A, out

    B> { data class Failure<out A>(val value: A) : Either<A, Nothing>() data class Success<out B>(val value: B) : Either<Nothing, B>() }
  68. @basdgrt Coding up an Either sealed class Either<out A, out

    B> { data class Failure<out A>(val value: A) : Either<A, Nothing>() data class Success<out B>(val value: B) : Either<Nothing, B>() } Can only be an instance of failure or success
  69. @basdgrt Coding up an Either sealed class Either<out A, out

    B> { data class Failure<out A>(val value: A) : Either<A, Nothing>() data class Success<out B>(val value: B) : Either<Nothing, B>() } Can only be an instance of failure or success Covariant generic types to represent the left and right compartments
  70. @basdgrt Coding up an Either sealed class Either<out A, out

    B> { data class Failure<out A>(val value: A) : Either<A, Nothing>() data class Success<out B>(val value: B) : Either<Nothing, B>() } Can only be an instance of failure or success Covariant generic types to represent the left and right compartments Right compartment is empty
  71. @basdgrt Coding up an Either sealed class Either<out A, out

    B> { data class Failure<out A>(val value: A) : Either<A, Nothing>() data class Success<out B>(val value: B) : Either<Nothing, B>() } Can only be an instance of failure or success Covariant generic types to represent the left and right compartments Right compartment is empty Left compartment is empty
  72. @basdgrt Updated function signature fun grindBeans(): Either<MachineFailure, CoffeeBeans>

  73. @basdgrt Updated function signature fun grindBeans(): Either<MachineFailure, CoffeeBeans> Function signature

    tells the full story
  74. @basdgrt Updated function signature fun grindBeans(): Either<MachineFailure, CoffeeBeans> Function signature

    tells the full story class CoffeeMachine { fun makeCoffee(): Coffee { val beans = grindBeans() return brew(beans) } }
  75. @basdgrt Updated function signature fun grindBeans(): Either<MachineFailure, CoffeeBeans> Function signature

    tells the full story class CoffeeMachine { fun makeCoffee(): Coffee { val beans = grindBeans() return brew(beans) } } Compilation failure
  76. @basdgrt Handling failures class CoffeeMachine { fun makeCoffee(): Either<MachineFailure, Coffee>

    { val beansEither = grindBeans() return when (beansEither) { is Either.Failure -> beansEither // instance of Failure is Either.Success -> Either.Success(brew(beansEither.value)) } } }
  77. @basdgrt Handling failures class CoffeeMachine { fun makeCoffee(): Either<MachineFailure, Coffee>

    { val beansEither = grindBeans() return when (beansEither) { is Either.Failure -> beansEither // instance of Failure is Either.Success -> Either.Success(brew(beansEither.value)) } } } Indicate that makeCoffee() can fail
  78. @basdgrt Handling failures class CoffeeMachine { fun makeCoffee(): Either<MachineFailure, Coffee>

    { val beansEither = grindBeans() return when (beansEither) { is Either.Failure -> beansEither // instance of Failure is Either.Success -> Either.Success(brew(beansEither.value)) } } } Indicate that makeCoffee() can fail Just return the failure
  79. @basdgrt Handling failures class CoffeeMachine { fun makeCoffee(): Either<MachineFailure, Coffee>

    { val beansEither = grindBeans() return when (beansEither) { is Either.Failure -> beansEither // instance of Failure is Either.Success -> Either.Success(brew(beansEither.value)) } } } Indicate that makeCoffee() can fail Just return the failure Wrap the result of brew(), we need to return an Either
  80. @basdgrt Map

  81. @basdgrt Visualise map

  82. @basdgrt 1. Take the value from the right side of

    the box Visualise map
  83. @basdgrt 1. Take the value from the right side of

    the box 2. Apply a function Visualise map
  84. @basdgrt 1. Take the value from the right side of

    the box 2. Apply a function Visualise map
  85. @basdgrt 1. Take the value from the right side of

    the box 2. Apply a function 3. Put the result back into the box Visualise map
  86. @basdgrt Implement map inline fun <A, B, C> Either<A, B>.map(fn:

    (B) -> C) = when (this) { is Either.Failure -> this is Either.Success -> Either.Success(fn(this.value)) }
  87. @basdgrt Implement map inline fun <A, B, C> Either<A, B>.map(fn:

    (B) -> C) = when (this) { is Either.Failure -> this is Either.Success -> Either.Success(fn(this.value)) } Takes a function to convert some object of type B to some object of type C
  88. @basdgrt Implement map inline fun <A, B, C> Either<A, B>.map(fn:

    (B) -> C) = when (this) { is Either.Failure -> this is Either.Success -> Either.Success(fn(this.value)) } Takes a function to convert some object of type B to some object of type C Just return the failure
  89. @basdgrt Implement map inline fun <A, B, C> Either<A, B>.map(fn:

    (B) -> C) = when (this) { is Either.Failure -> this is Either.Success -> Either.Success(fn(this.value)) } Takes a function to convert some object of type B to some object of type C Just return the failure Apply function and wrap the result
  90. @basdgrt Using map fun makeCoffee(): Either<MachineFailure, Coffee> { return grindBeans().map

    { brew(it) } }
  91. @basdgrt Brew can also fail… fun brew(beans: CoffeeBeans): Coffee fun

    makeCoffee(): Either<MachineFailure, Coffee> { return grindBeans().map { brew(it) } }
  92. @basdgrt Brew can also fail… fun brew(beans: CoffeeBeans): Either<MachineFailure, Coffee>

    fun makeCoffee(): Either<MachineFailure, Coffee> { return grindBeans().map { brew(it) } }
  93. @basdgrt Brew can also fail… fun brew(beans: CoffeeBeans): Either<MachineFailure, Coffee>

    fun makeCoffee(): Either<MachineFailure, Coffee> { return grindBeans().map { brew(it) } } Fails to compile…
  94. @basdgrt Brew can also fail… fun brew(beans: CoffeeBeans): Either<MachineFailure, Coffee>

    fun makeCoffee(): Either<MachineFailure, Coffee> { return grindBeans().map { brew(it) } } Fails to compile…
  95. @basdgrt Either inception

  96. @basdgrt Either inception 1. Take the value from the right

    side of the box
  97. @basdgrt Either inception 1. Take the value from the right

    side of the box 2. Apply a function
  98. @basdgrt Either inception 1. Take the value from the right

    side of the box 2. Apply a function
  99. @basdgrt Either inception 1. Take the value from the right

    side of the box 2. Apply a function 3. Put the result back into the box
  100. @basdgrt Flat map

  101. @basdgrt Visualise fl at map

  102. @basdgrt Visualise fl at map 1. Take the value from

    the right side of the box
  103. @basdgrt Visualise fl at map 1. Take the value from

    the right side of the box 2. Apply a function which returns a box
  104. @basdgrt Visualise fl at map 1. Take the value from

    the right side of the box 2. Apply a function which returns a box
  105. @basdgrt Visualise fl at map 1. Take the value from

    the right side of the box 2. Apply a function which returns a box 3. Return the new box as a result
  106. @basdgrt Implement fl at map inline fun <A, B, C>

    Either<A, B>.flatMap(fn: (B) -> Either<A, C>) = when (this) { is Either.Failure -> this is Either.Success -> fn(this.value) }
  107. @basdgrt Implement fl at map inline fun <A, B, C>

    Either<A, B>.flatMap(fn: (B) -> Either<A, C>) = when (this) { is Either.Failure -> this is Either.Success -> fn(this.value) } Takes a function to convert some object of type B to an Either<A, C>
  108. @basdgrt Implement fl at map inline fun <A, B, C>

    Either<A, B>.flatMap(fn: (B) -> Either<A, C>) = when (this) { is Either.Failure -> this is Either.Success -> fn(this.value) } Takes a function to convert some object of type B to an Either<A, C> Function returns an either, no need to wrap the result
  109. @basdgrt Using fl at map fun makeCoffee(): Either<MachineFailure, Coffee> {

    return grindBeans().flatMap { brew(it) } }
  110. @basdgrt Nesting fun brew(beans: CoffeeBeans, water: Water): Either<MachineFailure, Coffee>

  111. @basdgrt Nesting fun brew(beans: CoffeeBeans, water: Water): Either<MachineFailure, Coffee> fun

    boilWater(): Either<MachineFailure, Water>
  112. @basdgrt Nesting fun brew(beans: CoffeeBeans, water: Water): Either<MachineFailure, Coffee> fun

    boilWater(): Either<MachineFailure, Water> fun makeCoffee(): Either<MachineFailure, Coffee> { return grindBeans().flatMap { beans -> boilWater().flatMap { water -> brew(beans, water) } } }
  113. @basdgrt Monad Comprehensions

  114. @basdgrt Arrow-kt documentation “The purpose of monad comprehensions is to

    compose sequential chains of actions in a style that feels natural for programmers of all backgrounds.”
  115. @basdgrt Simple but effective inline fun <A, B> Either<A, B>.onFailure(fn:

    (Either.Failure<A>) -> Nothing): B = when(this) { is Either.Failure -> fn(this) is Either.Success -> value }
  116. @basdgrt Simple but effective inline fun <A, B> Either<A, B>.onFailure(fn:

    (Either.Failure<A>) -> Nothing): B = when(this) { is Either.Failure -> fn(this) is Either.Success -> value } Apply function if we are dealing with a failure
  117. @basdgrt Simple but effective inline fun <A, B> Either<A, B>.onFailure(fn:

    (Either.Failure<A>) -> Nothing): B = when(this) { is Either.Failure -> fn(this) is Either.Success -> value } Apply function if we are dealing with a failure Return unboxed value on success
  118. @basdgrt Simple but effective inline fun <A, B> Either<A, B>.onFailure(fn:

    (Either.Failure<A>) -> Nothing): B = when(this) { is Either.Failure -> fn(this) is Either.Success -> value } Apply function if we are dealing with a failure Return unboxed value on success fun makeCoffee(): Either<MachineFailure, Coffee> { val beans = grindBeans().onFailure { return it } val water = boilWater().onFailure { return it } return brew(beans, water) }
  119. @basdgrt Simple but effective inline fun <A, B> Either<A, B>.onFailure(fn:

    (Either.Failure<A>) -> Nothing): B = when(this) { is Either.Failure -> fn(this) is Either.Success -> value } Apply function if we are dealing with a failure Return unboxed value on success fun makeCoffee(): Either<MachineFailure, Coffee> { val beans = grindBeans().onFailure { return it } val water = boilWater().onFailure { return it } return brew(beans, water) } Return from within a lambda out of the enclosing function
  120. @basdgrt Arrow-kt fun makeCoffee() = IO.fx { val beans =

    grindBeans().bind() val water = boilWater().bind() brew(beans, water) }
  121. @basdgrt Handling failures

  122. @basdgrt Modeling failures sealed class MachineFailure { object NotEnoughBeans :

    MachineFailure() object MissingFilter : MachineFailure() }
  123. @basdgrt Fold inline fun <A, B, C> Either<A, B>.fold(onFailure: (A)

    -> C, onSuccess: (B) -> C) = when (this) { is Either.Failure -> onFailure(value) is Either.Success -> onSuccess(value) }
  124. @basdgrt Let’s help out our barista class Barista(private val machine:

    CoffeeMachine) { fun handleOrder() { machine.makeCoffee().fold( onSuccess = { serveToCustomer(it) }, onFailure = { when (it) { MachineFailure.MissingFilter -> TODO() MachineFailure.NotEnoughBeans -> TODO() } }) } }
  125. @basdgrt Let’s help out our barista Serve coffee on success

    class Barista(private val machine: CoffeeMachine) { fun handleOrder() { machine.makeCoffee().fold( onSuccess = { serveToCustomer(it) }, onFailure = { when (it) { MachineFailure.MissingFilter -> TODO() MachineFailure.NotEnoughBeans -> TODO() } }) } }
  126. @basdgrt Let’s help out our barista Handle errors on failure

    Serve coffee on success class Barista(private val machine: CoffeeMachine) { fun handleOrder() { machine.makeCoffee().fold( onSuccess = { serveToCustomer(it) }, onFailure = { when (it) { MachineFailure.MissingFilter -> TODO() MachineFailure.NotEnoughBeans -> TODO() } }) } }
  127. @basdgrt Let’s help out our barista Handle errors on failure

    Serve coffee on success class Barista(private val machine: CoffeeMachine) { fun handleOrder() { machine.makeCoffee().fold( onSuccess = { serveToCustomer(it) }, onFailure = { when (it) { MachineFailure.MissingFilter -> TODO() MachineFailure.NotEnoughBeans -> TODO() } }) } } When is only exhaustive if it’s used as an expression
  128. @basdgrt Making when exhaustive fun Any.exhaustive() = this.let { }

  129. @basdgrt Making when exhaustive fun Any.exhaustive() = this.let { }

    class Barista(private val machine: CoffeeMachine) { fun handleOrder() { machine.makeCoffee().fold( onSuccess = { serveToCustomer(it) }, onFailure = { when (it) { MachineFailure.MissingFilter -> TODO() MachineFailure.NotEnoughBeans -> TODO() }.exhaustive() }) } }
  130. @basdgrt We’ve come a long way

  131. @basdgrt We’ve come a long way Function signatures are correct

  132. @basdgrt We’ve come a long way Function signatures are correct

    Compiler forces us to handle the unhappy path
  133. @basdgrt We’ve come a long way Function signatures are correct

    Compiler forces us to handle the unhappy path We can design for compile time safety
  134. @basdgrt The elephant in the room Where do you convert

    an exception to an Either?
  135. @basdgrt The elephant in the room Where do you convert

    an exception to an Either? As close as possible to the function that produces the exception
  136. @basdgrt The elephant in the room Where do you convert

    an exception to an Either? As close as possible to the function that produces the exception At the boundaries of your application
  137. @basdgrt The elephant in the room Where do you convert

    an exception to an Either? As close as possible to the function that produces the exception At the boundaries of your application Accessing resources over HTTP
  138. @basdgrt The elephant in the room Where do you convert

    an exception to an Either? As close as possible to the function that produces the exception At the boundaries of your application Accessing resources over HTTP Saving to a database
  139. @basdgrt FAQ

  140. @basdgrt Did we just re-invent checked exceptions?

  141. @basdgrt Did we just re-invent checked exceptions? Yes and no…

  142. @basdgrt Did we just re-invent checked exceptions? Yes and no…

    Checked exceptions don’t work with higher order functions
  143. @basdgrt Did we just re-invent checked exceptions? Yes and no…

    Checked exceptions don’t work with higher order functions Either solution is cleaner
  144. @basdgrt Doesn’t the stdlib provide a Result type?

  145. @basdgrt Doesn’t the stdlib provide a Result type? Yes it

    does
  146. @basdgrt Doesn’t the stdlib provide a Result type? Yes it

    does It’s somewhat similar
  147. @basdgrt Doesn’t the stdlib provide a Result type? Yes it

    does It’s somewhat similar The failure side is always Throwable
  148. @basdgrt Doesn’t the stdlib provide a Result type? Yes it

    does It’s somewhat similar The failure side is always Throwable Not possible to do exhaustive failure checking
  149. @basdgrt What are the downsides of doing this?

  150. @basdgrt What are the downsides of doing this? Learning curve

  151. @basdgrt What are the downsides of doing this? Learning curve

    Imposes a coding style
  152. @basdgrt What are the downsides of doing this? Learning curve

    Imposes a coding style Might need an extra dependency (arrow-kt)
  153. @basdgrt What are the downsides of doing this? Learning curve

    Imposes a coding style Might need an extra dependency (arrow-kt) Hard to apply to existing systems
  154. @basdgrt twitter.com/basdgrt medium.com/@basdgrt github.com/basdgrt