0
0

「Unlocking the Power of Arrow 2 0 A Comprehensive Guide」 を見る

Posted at

KotlinConf 24 の以下
https://www.youtube.com/watch?v=JcFEI8_af3g&t=1s

Arrow -> https://arrow-kt.io/


TypedErrorHandling

Exception vs Typed Errors

例外は致命的で予期せぬ状況に対処し、型付きエラーはビジネスロジック内でのエラーを安全に管理するために使用されるべきだということ 👇

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
    }

How to model errors

  • CustomModels ( sealed hierarchies)
    • use case
    • Layering
  • Context Parameters

UseCase

sealed interface UserError
data class UserExists(val username: String): UserError
data object UsernameMissing : UserError

context(Raise<UsernameMissing>)
fun HttpRequest.username(): String

Service Layer

sealed interface UserRegistrationError
sealed interface UserError: UserRegistrationError
sealed interface PaymentError : UserRegistrationError

suspend fun Raise<UserRegistrationError>.route(
    request: HttpRequest
): HttpResponse {
    val name = request.username()
    val user = insertUser(name)
    user.receivePayment()
    return HttpResponse.CREATED
}

Context Parameter

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

エラーを独立して 複数 明示できる

Why raise

  • 組み合わせが容易
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()
}

Raiseが起こった時点(行)で処理が呼び出し元にもどる。 各行に Throwがあるようなイメージだとわかりやすい?

  • Raise = CancellationException
context(Raise<UserExists>)
fun transaction(): User =
    transaction {
        queries.insertUser("nomisrev")
    }

Coroutineで呼び出し元にCancellationException が流れるように、Raiseも 呼び出し元のながれていくので、キャンセルされたらどうするか を 呼び出し元で判断してもらうという仕組みは一緒

Resilience

Schedule

Retry 戦略 の書き方

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!!

👇

Schedule.exponential<SQLException>(BASE_DELAY)
    .and(Schedule.recurs(MAX_RETRIES))
    .jittered() // リトライ間隔のランダムな変動
    .doWhile { e: SQLException, _ -> e.isRetryable() }
    .retry { insertUser(name) }

Schedule で よく使いそうなのは たとえば 「5回 、成功するまで繰り返す」

Schedule.recurs<HttpResponse>(5)
    .doUntil { response, _ -> response.isCorrect() }
    .repeat { client.get("my-url") }

SageScope

同一関数ないで 二つの transactionが動いている という状態

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

ロールバックが難しいから大抵はよくない

Saga Transaction

マイクロサービスアーキテクチャ であるようなパターン を関数レベルで って意味で Saga?

context(Raise<UserrExists>, SagaScope)
suspend fun insertUserOrRollback(username: String): User
	= saga({
		insertUser(userName)
	}) { user -> deleteUser(user) }
context(Raise<UserRegistrationError>)
suspend fun route(request: HttpRequest): HttpResponse {
    val name = request.username()
    saga {
	    val user = insertUserOrRollback(name)
	    user.receivePayment()
    }.transact()
    return HttpResponse.CREATED
}

この辺りは Transaction による 分散 処理を ちゃんと制御するときに 役立ちそう

Optics

データ構造を便利にcopyする

val item = OrderItem(Product(id = 1, price = 1.5), quantity = 1)

item.copy(
    product = item.product.copy(
        price = item.product.price * 0.9
    )
)
👇
OrderItem.product.price.modify(item) { it * 1.1 }
data class Inventory(val items: List<Product>)

inventory.copy(
    items = inventory.items.map { product ->
        product.copy(price = product.price * 1.1)
    }
)
👇
Inventory.items.every.price.modify(inventory) { it * 1.1 }
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 * 1.1)
    )
}
👇
ErrorOrProduct.success.product.price.modify(errorOrProduct) { it * 1.1 }
val item = OrderItem(Product(id = 1, price = 1.5), quantity = 1)

item.copy {
    OrderItem.product.price.transform { it * 1.1 }
    OrderItem.quantity.set(5)
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0