În sistemele software complexe de tip bancar, logistic sau telecomunicații, codul care utilizează excepții runtime (throw IllegalStateException(...)) suferă de defecte majore de design: semnăturile de funcții mint dezvoltatorul (spun că returnează un User, dar ascund faptul că pot crăpa oricând), compilatorul nu poate garanta că toate erorile sunt tratate, iar crearea de instanțe Throwable umple inutil stiva de apel (stack trace overhead), degradând sever performanța.
Arrow-kt este biblioteca de referință pentru Programare Funcțională (FP) în limbajul Kotlin. Odată cu Arrow 1.2+ și compatibilitatea deplină Kotlin 2.0, Arrow a introdus noul DSL Raise<E>: un model revoluționar bazat pe continuări care oferă sintaxa intuitivă a excepțiilor (cu oprire la prima eroare), dar cu garanția deplină de tip la compilare și performanță identică cu salturile locale de cod!
# 1. Modelul Bazat pe Excepții Tradiționale:
# fun process(order: Order): ResultOrder {
# throw PaymentException() ---> Umple stack trace, rupe firul de execuție,
# } ---> Semnătura nu declară PaymentException!
# ---> Tratat doar prin try/catch la runtime.
# 2. Modelul Funcțional Arrow Raise DSL:
# fun Raise.process(order: Order): ResultOrder {
# raise(OrderError.PaymentFailed) ---> Oprește execuția local fără alocare pe heap!
# } ---> Tipul erorii este obligatoriu în semnătură!
# ---> Compilatorul forțează tratarea completă.
Tabel Comparativ: Metode de Tratare a Erorilor în Kotlin
| Tehnică Utilizată | Siguranță la Compilare | Overhead de Memorie / CPU | Acumulare Erori Multiple | Ergonomie Sintactică |
|---|---|---|---|---|
throw Exception |
Zero (Excepții neverificate pe JVM) | Foarte mare (construire completă a stivei de apel) | Nu (se oprește la prima excepție) | Cod imperativ clasic try/catch |
kotlin.Result<T> |
Parțială (Eroarea este întotdeauna doar Throwable) |
Redus (Value class pe JVM) | Nu nativ | Metode de tip mapCatching, getOrElse |
Arrow Either<E, A> |
Totală (Tipul E este specificat exact) |
Mediu (Alocare obiecte Left și Right) |
Da (prin zipOrAccumulate) |
Blocuri either { ... } și bind() |
Arrow Raise<E> DSL |
Totală (Eroarea E este receptor de context) |
Zero-Cost (Fără alocare pe heap, optimizat de K2) | Da (prin zipOrAccumulate cu NonEmptyList) |
Sintaxă directă imperativă raise(eroare) |
Implementare de Nivel Enterprise: Pipeline de Validare și Procesare Comandă
Exemplul de mai jos demonstrează modelarea domeniului cu Sealed Interfaces, validarea paralelă cu acumulare de erori multiple (NonEmptyList) și execuția tranzacțională a unei comenzi:
package ro.dexblog.arrow.domain
import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.nonEmptyListOf
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.zipOrAccumulate
// 1. Ierarhie strictă de erori de domeniu (Domain Errors)
sealed interface OrderError {
data class ValidationFailed(val errors: NonEmptyList) : OrderError
data class CustomerNotFound(val customerId: String) : OrderError
data class InsufficientFunds(val available: Double, val required: Double) : OrderError
data class InventoryUnavailable(val unavailableItems: List) : OrderError
}
data class OrderItem(val sku: String, val quantity: Int, val unitPrice: Double)
data class RawOrderDto(val customerId: String, val items: List)
data class ValidatedOrder(val customerId: String, val items: List, val total: Double)
data class CompletedOrder(val orderId: String, val totalAmount: Double, val status: String)
class OrderProcessingService {
// Validare primară cu acumularea tuturor erorilor simultan!
private fun Raise.validate(dto: RawOrderDto): ValidatedOrder {
zipOrAccumulate(
{ ensure(dto.customerId.isNotBlank()) { "ID-ul clientului nu poate fi gol!" } },
{ ensure(dto.items.isNotEmpty()) { "Comanda trebuie să aibă cel puțin o linie!" } },
{
dto.items.forEach { item ->
ensure(item.quantity > 0) { "Cantitatea pentru produsul ${item.sku} trebuie să fie pozitivă!" }
ensure(item.unitPrice > 0.0) { "Prețul pentru produsul ${item.sku} trebuie să fie valid!" }
}
}
) { _, _, _ ->
val total = dto.items.sumOf { it.quantity * it.unitPrice }
ValidatedOrder(dto.customerId, dto.items, total)
}
}
// Funcție de domeniu cu receptorul de context Raise
private fun Raise.chargeCustomer(customerId: String, amount: Double) {
val customerBalance = 150.0 // Simulare interogare cont
if (customerBalance < amount) {
raise(OrderError.InsufficientFunds(available = customerBalance, required = amount))
}
}
// Orchestrator tranzacțional complet expus ca funcție pură returnând Either
fun processOrder(dto: RawOrderDto): Either = either {
// Pasul 1: Validare date de intrare (Erorile sunt acumulate)
val validOrder = validate(dto)
// Pasul 2: Verificare sold client
chargeCustomer(validOrder.customerId, validOrder.total)
// Pasul 3: Rezervare stoc și finalizare
CompletedOrder(
orderId = "ORD-" + System.currentTimeMillis(),
totalAmount = validOrder.total,
status = "CONFIRMED"
)
}
}
fun main() {
val service = OrderProcessingService()
// Test 1: Comandă invalidă (acumulare erori multiple)
val invalidDto = RawOrderDto(customerId = "", items = emptyList())
val resultInvalid = service.processOrder(invalidDto)
println("Rezultat comandă invalidă:\n$resultInvalid\n")
// Test 2: Comandă cu fonduri insuficiente
val validDto = RawOrderDto(
customerId = "CUST-102",
items = listOf(OrderItem("SKU-LAPTOP", 1, 1200.0))
)
val resultFunds = service.processOrder(validDto)
println("Rezultat fonduri insuficiente:\n$resultFunds")
}