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

Railway Oriented Programming with Kotlin

Railway Oriented Programming with Kotlin

Many examples in Kotlin assume that you are always on the "happy path". But real-world applications must deal with a lot of types of errors: validation, network, and others.

So, how do you handle all of this?

Marcello Galhardo

June 05, 2019
Tweet

More Decks by Marcello Galhardo

Other Decks in Programming

Transcript

  1. Marcello Galhardo
    [email protected]
    A functional approach to error handling
    Railway Oriented
    Programming with Kotlin.

    View Slide

  2. This talk is not about code…
    …it is about a paradigm.

    View Slide

  3. “As a user I want to update my
    name and email address.”

    View Slide

  4. Button clicks send Action to ViewModel

    View Slide

  5. Button clicks send Action to ViewModel
    ViewModel invokes UseCase to validate
    and canonicalise Action data

    View Slide

  6. Button clicks send Action to ViewModel
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    UseCase ask Repository to update user data

    View Slide

  7. Button clicks send Action to ViewModel
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    UseCase ask Repository to update user data
    Returns result to UI

    View Slide

  8. Action.kt
    sealed class Action {
    data class UpdateUser(
    val id: UserId,
    val name: String,
    val email: String
    ) : Action()
    }

    View Slide

  9. Action.kt
    sealed class Action {
    data class UpdateUser(
    val id: UserId,
    val name: String,
    val email: String
    ) : Action()
    }

    View Slide

  10. Action.kt
    sealed class Action {
    data class UpdateUser(
    val id: UserId,
    val name: String,
    val email: String
    ) : Action()
    }

    View Slide

  11. View.kt
    // Action dispatcher in a View
    okButton.setOnClickListener {
    val name = nameTextField.value
    val email = emailTextField.value
    val action = Action.UpdateUser(userId, name, email)
    viewModel.dispatch(action)
    }

    View Slide

  12. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    validate(action)
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  13. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    validate(action)
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  14. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    validate(action)
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  15. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    validate(action)
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  16. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    validate(action)
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  17. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    validate(action)
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  18. Functions
    // ValidateUser.kt
    fun validate(action: Action.UpdateUser): Boolean
    // CanonicalizeEmail.kt
    fun canonicalizeEmail(action: Action.UpdateUser): String
    // UserRepository.kt
    @Throws(Exception::class)
    fun Repository.getUserById(id: UserId): User?
    @Throws(Exception::class)
    fun Repository.update(user: User)

    View Slide

  19. Functions
    // ValidateUser.kt
    fun validate(action: Action.UpdateUser): Boolean
    // CanonicalizeEmail.kt
    fun canonicalizeEmail(action: Action.UpdateUser): String
    // UserRepository.kt
    @Throws(Exception::class)
    fun Repository.getUserById(id: UserId): User?
    @Throws(Exception::class)
    fun Repository.update(user: User)

    View Slide

  20. Functions
    // ValidateUser.kt
    fun validate(action: Action.UpdateUser): Boolean
    // CanonicalizeEmail.kt
    fun canonicalizeEmail(action: Action.UpdateUser): String
    // UserRepository.kt
    @Throws(Exception::class)
    fun Repository.getUserById(id: UserId): User?
    @Throws(Exception::class)
    fun Repository.update(user: User)

    View Slide

  21. Functions
    // ValidateUser.kt
    fun validate(action: Action.UpdateUser): Boolean
    // CanonicalizeEmail.kt
    fun canonicalizeEmail(action: Action.UpdateUser): String
    // UserRepository.kt
    @Throws(Exception::class)
    fun Repository.getUserById(id: UserId): User?
    @Throws(Exception::class)
    fun Repository.update(user: User)

    View Slide

  22. What do you do when
    something goes
    wrong?

    View Slide

  23. “As a user I want to update my
    name and email address…
    …and see error messages
    when something goes wrong!"

    View Slide

  24. “As a user I want to update my
    name and email address…
    …and see error messages
    when something goes wrong!"

    View Slide

  25. Button clicks send Action to ViewModel
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    UseCase ask Repository to update user data
    Returns result to UI

    View Slide

  26. Button clicks send Action to ViewModel
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    UseCase ask Repository to update user data
    Returns result to UI

    View Slide

  27. Name is blank or email not valid
    Button clicks send Action to ViewModel
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    UseCase ask Repository to update user data
    Returns result to UI

    View Slide

  28. Name is blank or email not valid
    Button clicks send Action to ViewModel
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    UseCase ask Repository to update user data
    Returns result to UI

    View Slide

  29. Name is blank or email not valid
    User not found
    Button clicks send Action to ViewModel
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    UseCase ask Repository to update user data
    Returns result to UI

    View Slide

  30. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    validate(action)
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  31. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    validate(action)
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  32. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    val isValid = validate(action)
    if (!isValid) {
    return Result.error(UserNotValidException())
    }
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  33. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    val isValid = validate(action)
    if (!isValid) {
    return Result.error(UserNotValidException())
    }
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  34. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    val isValid = validate(action)
    if (!isValid) {
    return Result.error(UserNotValidException())
    }
    val canonicalizedEmail = canonicalizeEmail(action)
    val user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    return Result.success(user)
    }

    View Slide

  35. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    val isValid = validate(action)
    if (!isValid) {
    return Result.error(UserNotValidException())
    }
    val canonicalizedEmail = canonicalizeEmail(action)
    var user = null
    try {
    user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    } catch (error: Exception) {
    return Result.error(error)
    }
    return Result.success(user)
    }

    View Slide

  36. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    val isValid = validate(action)
    if (!isValid) {
    return Result.error(UserNotValidException())
    }
    val canonicalizedEmail = canonicalizeEmail(action)
    var user = null
    try {
    user = repository.getUserById(id).copy(
    name = action.name,
    email = canonicalizedEmail
    )
    repository.update(user)
    } catch (error: Exception) {
    return Result.error(error)
    }
    return Result.success(user)
    }

    View Slide

  37. 7 lines added, more 140%

    View Slide

  38. • Each use case will be equivalent to a single function
    • The function will return a sum type with two cases: Success or Failure
    • The use case function will be built from a series of smaller functions,
    each representing one step in a data flow
    • The errors from each step will be combined into a single failure

    View Slide

  39. • Each use case will be equivalent to a single function
    • The function will return a sum type with two cases: Success, Failure
    • The use case function will be built from a series of smaller functions,
    each representing one step in a data flow
    • The errors from each step will be combined into a single failure

    View Slide

  40. • Each use case will be equivalent to a single function
    • The function will return a sum type with two cases: Success, Failure
    • The use case function will be built from a series of smaller functions,
    each representing one step in a data flow
    • The errors from each step will be combined into a single failure

    View Slide

  41. • Each use case will be equivalent to a single function
    • The function will return a sum type with two cases: Success, Failure
    • The use case function will be built from a series of smaller functions,
    each representing one step in a data flow
    • Each step will be combined into a single result

    View Slide

  42. The following Result class and
    methods are simplified/custom
    versions for easy understanding

    View Slide

  43. How do you return a sum type
    with two cases?

    View Slide

  44. bit.do/kotlin-std-result
    sealed class Result {
    data class Success(
    val value: Value
    ): Result
    data class Failure(
    val error: Exception
    ): Result
    }

    View Slide

  45. bit.do/kotlin-std-result
    sealed class Result {
    data class Success(
    val value: Value
    ): Result
    data class Failure(
    val error: Exception
    ): Result
    }

    View Slide

  46. bit.do/kotlin-std-result
    sealed class Result {
    data class Success(
    val value: Value
    ): Result
    data class Failure(
    val error: Exception
    ): Result
    }

    View Slide

  47. bit.do/kotlin-std-result
    sealed class Result {
    data class Success(
    val value: Value
    ): Result
    data class Failure(
    val error: String
    ): Result
    }

    View Slide

  48. How to combine each step into
    a single result?

    View Slide

  49. public inline fun Result.map(transform: (value: T) -> R): Result {
    return when {
    isSuccess -> Result.success(transform(value as T))
    else -> Result(value)
    }
    }
    bit.do/kotlin-std-result

    View Slide

  50. public inline fun Result.map(transform: (value: T) -> R): Result {
    return when {
    isSuccess -> Result.success(transform(value as T))
    else -> Result(value)
    }
    }
    bit.do/kotlin-std-result

    View Slide

  51. public inline fun Result.map(transform: (value: T) -> R): Result {
    return when {
    isSuccess -> Result.success(transform(value as T))
    else -> Result(value)
    }
    }
    bit.do/kotlin-std-result

    View Slide

  52. public inline fun Result.map(transform: (value: T) -> R): Result {
    return when {
    isSuccess -> Result.success(transform(value as T))
    else -> Result(value)
    }
    }
    bit.do/kotlin-std-result

    View Slide

  53. public inline fun Result.flatMap(
    transform: (value: T) -> Result
    ): Result {
    return when {
    isSuccess -> transform(value as T)
    else -> Result(value)
    }
    }
    bit.do/kotlin-std-result

    View Slide

  54. public inline fun Result.flatMap(
    transform: (value: T) -> Result
    ): Result {
    return when {
    isSuccess -> transform(value as T)
    else -> Result(value)
    }
    }
    bit.do/kotlin-std-result

    View Slide

  55. public inline fun Result.flatMap(
    transform: (value: T) -> Result
    ): Result {
    return when {
    isSuccess -> transform(value as T)
    else -> Result(value)
    }
    }
    bit.do/kotlin-std-result

    View Slide

  56. public inline fun Result.flatMap(
    transform: (value: T) -> Result
    ): Result {
    return when {
    isSuccess -> transform(value as T)
    else -> Result(value)
    }
    }
    bit.do/kotlin-std-result

    View Slide

  57. ViewModel invokes UseCase to validate
    and canonicalise Action data

    View Slide

  58. ViewModel invokes UseCase to validate
    and canonicalise Action data
    Failure Success

    View Slide

  59. ViewModel invokes UseCase to validate
    and canonicalise Action data
    Failure
    UseCase ask Repository to update user data
    ?
    Success

    View Slide

  60. Success
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    UseCase ask Repository to update user data
    Failure
    Success
    Failure
    ?

    View Slide

  61. Success
    ViewModel invokes UseCase to validate
    and canonicalise Action data
    Failure
    UseCase ask Repository to update user data
    Failure
    Success

    View Slide

  62. GetUserById.kt
    fun getUserById(input: Action.UpdateUser): Result {
    val user = repository.getUserByIdOrNull(input.id)
    return when {
    user == null -> Result.failure("User not found.")
    else -> Result.success(user)
    }
    }

    View Slide

  63. NameNotBlank.kt
    fun nameNotBlank(user: User) = when {
    user.name == "" -> Result.failure("Name must not be blank.")
    else -> Result.success(user)
    }

    View Slide

  64. NameIsBelowMaxLength.kt
    fun nameIsBelowMaxLength(user: User) = when {
    user.name.length > 50 -> Result.failure("Name must not be longer than 50 chars.")
    else -> Result.success(user)
    }

    View Slide

  65. EmailNotBlank.kt
    fun emailNotBlank(user: User) = when {
    user.email == "" -> Result.failure("Email must not be blank.")
    else -> Result.success(user)
    }

    View Slide

  66. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    return getUserById(action.id)
    .map { user -> user.copy(name = action.name) }
    .map { user -> user.copy(email = action.email) }
    .map(::nameNotBlank)
    .map(::nameIsBelowMaxLength)
    .map(::emailNotBlank)
    .map(::canonicalizeEmail)
    .map(::saveUser)
    }

    View Slide

  67. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    return getUserById(action.id)
    .map { user -> user.copy(name = action.name) }
    .map { user -> user.copy(email = action.email) }
    .map(::nameNotBlank)
    .map(::nameIsBelowMaxLength)
    .map(::emailNotBlank)
    .map(::canonicalizeEmail)
    .map(::saveUser)
    }

    View Slide

  68. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    return getUserById(action.id)
    .map { user -> user.copy(name = action.name) }
    .map { user -> user.copy(email = action.email) }
    .map(::nameNotBlank)
    .map(::nameIsBelowMaxLength)
    .map(::emailNotBlank)
    .map(::canonicalizeEmail)
    .map(::saveUser)
    }

    View Slide

  69. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    return getUserById(action.id)
    .map { user -> user.copy(name = action.name) }
    .map { user -> user.copy(email = action.email) }
    .flatMap(::nameNotBlank)
    .flatMap(::nameIsBelowMaxLength)
    .flatMap(::emailNotBlank)
    .map(::canonicalizeEmail)
    .map(::saveUser)
    }

    View Slide

  70. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    return getUserById(action.id)
    .map { user -> user.copy(name = action.name) }
    .map { user -> user.copy(email = action.email) }
    .flatMap(::nameNotBlank)
    .flatMap(::nameIsBelowMaxLength)
    .flatMap(::emailNotBlank)
    .flatMap(::canonicalizeEmail)
    .map(::saveUser)
    }

    View Slide

  71. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    return getUserById(action.id)
    .map { user -> user.copy(name = action.name) }
    .map { user -> user.copy(email = action.email) }
    .flatMap(::nameNotBlank)
    .flatMap(::nameIsBelowMaxLength)
    .flatMap(::emailNotBlank)
    .flatMap(::canonicalizeEmail)
    .flatMap(::saveUser)
    }

    View Slide

  72. UpdateUser.kt
    fun updateUser(
    action: Action.UpdateUser
    ): Result {
    return getUserById(action.id)
    .map { user -> user.copy(name = action.name) }
    .map { user -> user.copy(email = action.email) }
    .flatMap(::nameNotBlank)
    .flatMap(::nameIsBelowMaxLength)
    .flatMap(::emailNotBlank)
    .flatMap(::canonicalizeEmail)
    .flatMap(::saveUser)
    }

    View Slide

  73. ViewModel.kt
    fun dispatch(action: Action) {
    useCase.invoke()
    .onSuccess {
    // Do something cool!
    }.onFailure {
    // Ops, something went wrong…
    }
    }

    View Slide

  74. ViewModel.kt
    fun dispatch(action: Action) {
    updateUser(action)
    .onSuccess {
    // Do something cool!
    }.onFailure {
    // Ops, something went wrong…
    }
    }

    View Slide

  75. ViewModel.kt
    fun dispatch(action: Action) {
    updateUser(action)
    .onSuccess { value ->
    // Do something cool!
    }.onFailure {
    // Ops, something went wrong…
    }
    }

    View Slide

  76. ViewModel.kt
    fun dispatch(action: Action) {
    updateUser(action)
    .onSuccess { value ->
    // Do something cool!
    }.onFailure { error ->
    // Ops, something went wrong…
    }
    }

    View Slide

  77. Recap

    View Slide

  78. • A strategy for handling errors in a functional way
    • Design for errors: unhappy paths are requirements too
    • Do not use Exception handling for Control Flow.
    “Do or do not, there is no try”

    View Slide

  79. • A strategy for handling errors in a functional way
    • Design for errors: unhappy paths are requirements too
    • Do not use Exception handling for Control Flow.
    “Do or do not, there is no try”

    View Slide

  80. • A strategy for handling errors in a functional way
    • Design for errors: unhappy paths are requirements too
    • Do not use Exception handling for Control Flow
    “Do or do not, there is no try”

    View Slide

  81. • A strategy for handling errors in a functional way
    • Design for errors: unhappy paths are requirements too
    • Do not use Exception handling for Control Flow
    “Do or do not, there is no try.”

    View Slide

  82. Marcello Galhardo
    [email protected]
    Any questions?
    Thank you.

    View Slide