Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Button clicks send Action to ViewModel

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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)

Slide 19

Slide 19 text

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)

Slide 20

Slide 20 text

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)

Slide 21

Slide 21 text

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)

Slide 22

Slide 22 text

What do you do when something goes wrong?

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

7 lines added, more 140%

Slide 38

Slide 38 text

• 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

Slide 39

Slide 39 text

• 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

Slide 40

Slide 40 text

• 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

Slide 41

Slide 41 text

• 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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

How do you return a sum type with two cases?

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

How to combine each step into a single result?

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

ViewModel invokes UseCase to validate and canonicalise Action data

Slide 58

Slide 58 text

ViewModel invokes UseCase to validate and canonicalise Action data Failure Success

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Recap

Slide 78

Slide 78 text

• 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”

Slide 79

Slide 79 text

• 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”

Slide 80

Slide 80 text

• 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”

Slide 81

Slide 81 text

• 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.”

Slide 82

Slide 82 text

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