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