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

Unlocking the Power of Arrow 2.0

Simon Vergauwen
May 23, 2024
170

Unlocking the Power of Arrow 2.0

We’ll dive into Arrow 2.0, a transformative update to Kotlin's functional programming landscape. Join the maintainer for an enlightening tour of Arrow 2.0's advancements, including typed error handling for safer code, seamless integration with Kotlin Coroutines for concurrent programming, resilient programming techniques, and elegant approaches to working with immutable data structures. Arrow 2.0 represents a significant milestone in our journey to make Functional Programming in Kotlin truly idiomatic, simple, and elegant to empower developers to write expressive, efficient, and reliable code. Whether you're a seasoned functional programmer or a Kotlin enthusiast, this session promises practical insights into mastering Arrow 2.0 and navigating the functional frontier in Kotlin.

Simon Vergauwen

May 23, 2024
Tweet

Transcript

  1. AutoClose DataSource(config).use { dataSource > Tracing().use { tracing > /

    } } autoCloseScope { val dataSource = install(DataSource(config)) val tracing = install(Tracing()) // use dataSource & tracing } Why? ΛRROW
  2. AutoClose fun <A> runBlocking( block: suspend CoroutineScope.() > ): A

    How? inline fun <A> autoCloseScope( block: AutoCloseScope.() -> A ): A ΛRROW
  3. AutoCloseScope interface AutoCloseScope { fun <A : AutoCloseable> install(autoCloseable: A):

    A } ‣Success: null ‣Cancelled: CancellationException ‣Failed: Throwable Bring your own types! ΛRROW fun <A> autoClose( acquire: () -> A, release: (A, Throwable?) -> Unit ): A
  4. ΛRROW ‣ Raise ‣ Either ‣ NonEmptyList & Set ‣

    Ior ‣ Tuple (4-9) ‣ Option AutoClose
  5. Typed Error Handling ΛRROW Why? class UserExists(message: String): RuntimeException(message) fun

    insertUser(username: String): User = throw UserExists("User $username already exists.”)
  6. Typed Error Handling interface Raise<in Error> { fun raise(r: Error):

    Nothing / / bind, ensure, ensureNotNull, etc. } ΛRROW DSL
  7. Typed Error Handling ΛRROW How? data class UserExists(val username: String)

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

    String) context(Raise<UserExists>) fun insertUser(username: String): User = raise(UserExists(username))
  9. Typed Error Handling inline fun <Error, A> recover( block: Raise<Error>.()

    -> A, recover: (error: Error) -> A, ): A ΛRROW DSL
  10. Typed Error Handling fun Raise<UserExists>.insertUser(username: String): User = raise(UserExists(username)) ΛRROW

    recover({ // Raise<UserExists>.() -> User insertUser(“nomisrev") }) { error: UserExists -> null } DSL
  11. 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?
  12. Typed Error Handling ΛRROW Example: Exception & Typed context(Raise<UserExists>) suspend

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

    fun insertUser(username: String): User = try { queries.insert(username) } catch (e: SQLException) { if (e.isUniqueViolation()) raise(UserExists(username)) else throw e }
  14. Modelling errors: Sealed hierarchy sealed interface UserError data class UserExists(val

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

    username: String): UserError data object UsernameMissing : UserError ΛRROW Use-case context(Raise<UsernameMissing>) fun HttpRequest.username(): String
  16. Modelling errors: Sealed hierarchy sealed interface PaymentError data object ExpiredCard

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

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

    UserRegistrationError sealed interface PaymentError : UserRegistrationError ΛRROW ( Service) Layering
  19. Context Parameters suspend fun Raise<UserRegistrationError>.route( request: HttpRequest ): HttpResponse {

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

    val name = request.username() val user = insertUser(name) user.receivePayment() return HttpResponse.CREATED } ΛRROW Independent errors
  21. Typed Error Handling ΛRROW Why Raise DSL? A? ≈ Option<A

    > suspend () -> A ≈ Mono<A > , Single<A > , … suspend FlowCollector<A > .() -> Unit Context Parameters ( KEEP - 367 )
  22. 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!
  23. Typed Error Handling context(Raise<String>) 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!
  24. 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
  25. 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?
  26. Schedule Schedule.recurs<HttpResponse>(5) .doUntil { response, _ -> response.isCorrect() } .repeat

    { client.get("my-url") } ΛRROW Repeat 5 times, or until correct result
  27. Schedule Split Schedule from logic Allows easily building complex Schedules

    Reuse for repeating, and retrying BONUS : Easy to replace with testing ΛRROW Conclusion
  28. SagaScope ΛRROW Why? context(Raise<UserRegistrationError>) suspend fun route(request: HttpRequest): HttpResponse {

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

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

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

    { val name = request.username() val user = insertUser(name) user.receivePayment() return HttpResponse.CREATED }
  32. SagaScope ΛRROW Why? fun Routing.premiumUser() = post("/premiumuser/{username}") { recover({ /

    / Raise<UserRegistrationError>.() - > newSuspendedTransaction { // Transaction.() -> route(call) } }) { error -> handleError(error) } }
  33. Optics data class Product(val id: Long, val price: Double) data

    class OrderItem(val product: Product, val quantity: Int) Why? ΛRROW
  34. 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
  35. 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 }
  36. Optics How? ΛRROW data class Inventory(val items: List<Product>) inventory.copy( items

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

    class Inventory(val items: List<Product>) inventory.copy( items = inventory.items.map { product > product.copy(price = product.price * 1.1) } )
  38. 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) ) }
  39. 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 }
  40. 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) }
  41. 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
  42. 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!