Slide 1

Slide 1 text

Unlocking the Power of Arrow 2.0 Simon Vergauwen @vergauwen_simon

Slide 2

Slide 2 text

ΛRROW Who am I? nomisRev vergauwen_simon vergauwensimon arrow-kt.io

Slide 3

Slide 3 text

ΛRROW

Slide 4

Slide 4 text

ΛRROW AutoClose

Slide 5

Slide 5 text

AutoClose DataSource(config).use { dataSource - > Tracing().use { tracing - > / / use dataSource & tracing } } Why? ΛRROW

Slide 6

Slide 6 text

AutoClose DataSource(config).use { dataSource > Tracing().use { tracing > / } } autoCloseScope { val dataSource = install(DataSource(config)) val tracing = install(Tracing()) // use dataSource & tracing } Why? ΛRROW

Slide 9

Slide 9 text

AutoCloseScope interface AutoCloseScope { fun install(autoCloseable: A): A } How? ΛRROW

Slide 10

Slide 10 text

AutoCloseScope interface AutoCloseScope { fun install(autoCloseable: A): A } ΛRROW Bring your own types!

Slide 12

Slide 12 text

AutoCloseScope autoCloseScope { val dataSource = install(DataSource(config)) val database = install(Tracing()) // use database & tracing } DSL ΛRROW

Slide 13

Slide 13 text

AutoCloseScope Same guarantees as AutoCloseable.use Imperative code, no nesting Bring your own types! Conclusion ΛRROW

Slide 14

Slide 14 text

ΛRROW ‣ Raise ‣ Either ‣ NonEmptyList & Set ‣ Ior ‣ Tuple (4-9) ‣ Option AutoClose

Slide 15

Slide 15 text

ΛRROW by CashApp AutoClose

Slide 16

Slide 16 text

Typed Error Handling ΛRROW Why? class UserExists(message: String): RuntimeException(message) fun insertUser(username: String): User = throw UserExists("User $username already exists.”)

Slide 17

Slide 17 text

Typed Error Handling ΛRROW Why? try { insertUser(1) } catch (e: UserExists) { recover(e) }

Slide 18

Slide 18 text

Typed Error Handling ΛRROW Why? try { insertUser(1) } catch (e: UserExists) { recover(e) }

Slide 19

Slide 19 text

Typed Error Handling interface Raise { fun raise(r: Error): Nothing / / bind, ensure, ensureNotNull, etc. } ΛRROW DSL

Slide 20

Slide 20 text

Typed Error Handling ΛRROW How? data class UserExists(val username: String) fun Raise.insertUser(username: String): User = raise(UserExists(username))

Slide 21

Slide 21 text

Typed Error Handling ΛRROW Context Parameters data class UserExists(val username: String) context(Raise) fun insertUser(username: String): User = raise(UserExists(username))

Slide 22

Slide 22 text

Typed Error Handling inline fun recover( block: Raise.() -> A, recover: (error: Error) -> A, ): A ΛRROW DSL

Slide 23

Slide 23 text

Typed Error Handling fun Raise.insertUser(username: String): User = raise(UserExists(username)) ΛRROW recover({ // Raise.() -> User insertUser(“nomisrev") }) { error: UserExists -> null } DSL

Slide 24

Slide 24 text

When truly exceptional Fatal exception ( ThreadDeath, VirtualMachineError, …) Unexpected exception, non-business related Type-safety helps protect what you care about Prevent forgetting about business errors Typed Error Handling ΛRROW FAQ : Exception vs Typed Errors?

Slide 25

Slide 25 text

Typed Error Handling ΛRROW Example: Exception & Typed context(Raise) suspend fun insertUser(username: String): User = try { queries.insert(username) } catch (e: SQLException) { }

Slide 26

Slide 26 text

Typed Error Handling ΛRROW Example: Exception & Typed context(Raise) suspend fun insertUser(username: String): User = try { queries.insert(username) } catch (e: SQLException) { if (e.isUniqueViolation()) raise(UserExists(username)) else throw e }

Slide 27

Slide 27 text

Custom models (sealed hierarchies) Use case Layering Context Parameters Typed Error Handling ΛRROW FAQ : How to model errors

Slide 28

Slide 28 text

Modelling errors: Sealed hierarchy sealed interface UserError data class UserExists(val username: String): UserError data object UsernameMissing : UserError ΛRROW Use-case

Slide 29

Slide 29 text

Modelling errors: Sealed hierarchy sealed interface UserError data class UserExists(val username: String): UserError data object UsernameMissing : UserError ΛRROW Use-case context(Raise) fun HttpRequest.username(): String

Slide 30

Slide 30 text

Modelling errors: Sealed hierarchy sealed interface PaymentError data object ExpiredCard : PaymentError data object InsufficientFunds : PaymentError ΛRROW Use-case

Slide 31

Slide 31 text

Modelling errors: Sealed hierarchy sealed interface PaymentError data object ExpiredCard : PaymentError data object InsufficientFunds : PaymentError ΛRROW Use-case context(Raise) fun User.receivePayment(): Unit

Slide 32

Slide 32 text

Modelling errors: Sealed hierarchy sealed interface UserRegistrationError sealed interface UserError: UserRegistrationError sealed interface PaymentError : UserRegistrationError ΛRROW ( Service) Layering

Slide 33

Slide 33 text

Context Parameters suspend fun Raise.route( request: HttpRequest ): HttpResponse { } ΛRROW ( Service) Layering

Slide 34

Slide 34 text

Context Parameters suspend fun Raise.route( request: HttpRequest ): HttpResponse { val name = request.username() val user = insertUser(name) user.receivePayment() return HttpResponse.CREATED } ΛRROW ( Service) Layering

Slide 35

Slide 35 text

Context Parameters context(Raise, Raise) suspend fun route(request: HttpRequest): HttpResponse { val name = request.username() val user = insertUser(name) user.receivePayment() return HttpResponse.CREATED } ΛRROW Independent errors

Slide 36

Slide 36 text

Slide 37

Slide 37 text

Typed Error Handling suspend fun refresh(redis: Redis, azure: Azure, custom: Custom) { val message = azure.reactor().awaitSingle() val cache = redis.future().await() delay(100) val res = custom.async().value() TODO() } ΛRROW Combine all the things!

Slide 38

Slide 38 text

Typed Error Handling context(Raise) fun everything() { val x = 1.right().bind() val y = ensureNotNull(2) { "Value was null" } ensure(y >= 0) { "y should be >= 0" } val z = quiverOutcome().value() TODO() } ΛRROW Combine all the things!

Slide 39

Slide 39 text

context(Raise) fun transaction(): User = transaction { queries.insertUser("nomisrev") } ΛRROW Typed Error Handling Raise = CancellationException

Slide 40

Slide 40 text

Typed Error Handling ΛRROW Compile-time check unhappy path Bring your own types! Automatic interoperability with ecosystem Elegantly fits into language Many different error handling strategies Conclusion

Slide 41

Slide 41 text

ΛRROW

Slide 42

Slide 42 text

ΛRROW Resilience Fx Coroutines Collectors STM Resource

Slide 43

Slide 43 text

Schedule var count = 0 var user: User? = null while (isActive) { try { result = insertUser(name) break } catch (e: SQLException) { if (count >= MAX_RETRIES) throw e else delay(BASE_DELAY * 2.0.pow(count ++ )) } } return user ! ! ΛRROW Why?

Slide 44

Slide 44 text

Schedule Schedule.exponential(BASE_DELAY) .and(Schedule.recurs(MAX_RETRIES)) .retry { insertUser(name) } ΛRROW How?

Slide 45

Slide 45 text

Schedule Schedule.exponential(BASE_DELAY) .and(Schedule.recurs(MAX_RETRIES)) .jittered() .retry { insertUser(name) } ΛRROW Avoid overlapping retries

Slide 46

Slide 46 text

Schedule Schedule.exponential(BASE_DELAY) .and(Schedule.recurs(MAX_RETRIES)) .jittered() .doWhile { e: SQLException, _ -> e.isRetryable() } .retry { insertUser(name) } ΛRROW Retry while error is retryable

Slide 47

Slide 47 text

Schedule Schedule.exponential(BASE_DELAY) .and(Schedule.recurs(MAX_RETRIES)) .jittered() .doWhile { e: PaymentError, _ -> e.isRetryable() } .retryRaise { raise(ExpiredCard) } ΛRROW Retrying Raise

Slide 48

Slide 48 text

Schedule Schedule.recurs(5) .doUntil { response, _ -> response.isCorrect() } .repeat { client.get("my-url") } ΛRROW Repeat 5 times, or until correct result

Slide 49

Slide 49 text

Schedule Split Schedule from logic Allows easily building complex Schedules Reuse for repeating, and retrying BONUS : Easy to replace with testing ΛRROW Conclusion

Slide 50

Slide 50 text

SagaScope ΛRROW Why? context(Raise) suspend fun route(request: HttpRequest): HttpResponse { val name = request.username() val user = insertUser(name) user.receivePayment() return HttpResponse.CREATED }

Slide 51

Slide 51 text

SagaScope ΛRROW Why? context(Raise, SagaScope) suspend fun insertUserOrRollback( username: String ): User

Slide 52

Slide 52 text

SagaScope ΛRROW Why? context(Raise, SagaScope) suspend fun insertUserOrRollback( username: String ): User = saga({ insertUser(username) }) { user -> deleteUser(user) }

Slide 53

Slide 53 text

SagaScope ΛRROW Why? context(Raise) suspend fun route(request: HttpRequest): HttpResponse { val name = request.username() val user = insertUserOrRollback(name) user.receivePayment() return HttpResponse.OK }

Slide 54

Slide 54 text

SagaScope ΛRROW Why? context(Raise) suspend fun route(request: HttpRequest): HttpResponse { val name = request.username() saga { val user = insertUserOrRollback(name) user.receivePayment() }.transact() return HttpResponse.OK }

Slide 55

Slide 55 text

SagaScope ΛRROW Why? context(Raise, Transaction) suspend fun route(request: HttpRequest): HttpResponse { val name = request.username() val user = insertUser(name) user.receivePayment() return HttpResponse.CREATED }

Slide 56

Slide 56 text

SagaScope ΛRROW Why? fun Routing.premiumUser() = post("/premiumuser/{username}") { recover({ / / Raise.() - > newSuspendedTransaction { // Transaction.() -> route(call) } }) { error -> handleError(error) } }

Slide 57

Slide 57 text

SagaScope Saga pattern DSL Avoid distributed transactions Builder for transactional systems ΛRROW Conclusion

Slide 58

Slide 58 text

ΛRROW

Slide 59

Slide 59 text

ΛRROW Compose KSP Reflect

Slide 60

Slide 60 text

Optics data class Product(val id: Long, val price: Double) data class OrderItem(val product: Product, val quantity: Int) Why? ΛRROW

Slide 61

Slide 61 text

Optics val item = OrderItem(Product(id = 1, price = 1.5), quantity = 1) item.copy( product = item.product.copy( price = item.product.price * 0.9 ) ) Why? ΛRROW

Slide 62

Slide 62 text

Optics val item = OrderItem(Product(id = 1, price = 1.5), quantity = 1) item.copy( product = item.product.copy( price = item.product.price * 0.9 ) ) Why? ΛRROW OrderItem.product.price.modify(item) { it * 1.1 }

Slide 63

Slide 63 text

Optics How? ΛRROW data class Inventory(val items: List) inventory.copy( items = inventory.items.map { product -> product.copy(price = product.price * 1.1) } )

Slide 64

Slide 64 text

Optics How? ΛRROW Inventory.items.every.price.modify(inventory) { it * 1.1 } data class Inventory(val items: List) inventory.copy( items = inventory.items.map { product > product.copy(price = product.price * 1.1) } )

Slide 65

Slide 65 text

Optics How? ΛRROW sealed interface ErrorOrProduct data class Success(val product: Product) : ErrorOrProduct data class Failed(val error: Throwable) : ErrorOrProduct when(errorOrProduct) { is Failed -> errorOrProduct is Success - > Success( errorOrProduct.product .copy(price = errorOrProduct.product.price * 0.9) ) }

Slide 66

Slide 66 text

sealed interface ErrorOrProduct data class Success(val product: Product) : ErrorOrProduct data class Failed(val error: Throwable) : ErrorOrProduct when(errorOrProduct) { is Failed > is Success > errorOrProduct.product .copy(price = errorOrProduct.product.price * 1.1) ) } Optics How? ΛRROW ErrorOrProduct.success.product.price.modify(errorOrProduct) { it * 1.1 }

Slide 67

Slide 67 text

Optics Multiple fields ΛRROW val item = OrderItem(Product(id = 1, price = 1.5), quantity = 1) item.copy { OrderItem.product.price.transform { it * 1.1 } OrderItem.quantity.set(5) }

Slide 68

Slide 68 text

Optics Focus on business logic, and paths Simplify complex immutable transformations KEEP - 237 : Immutability and value classes KT - 44653 by Roman Elisarov “Nicer data transformation with KopyKat and Optics” by Alejandro Serrano ΛRROW Conclusion

Slide 69

Slide 69 text

Conclusion What did we learn? ΛRROW Kotlin ❤ DSLs Arrow ❤ Kotlin Small feature specific modules Plug-n-Play: What you want, when you want Much more to learn!

Slide 70

Slide 70 text

ΛRROW slack-chats.kotlinlang.org/c/arrow arrow-kt.io github.com/arrow-kt github.com/nomisRev/KotlinConf2024Example Resources Much more to learn!

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

Thank you, and don’t forget to vote @vergauwen_simon