nw-fp-kotlin
Kotlin language-specific patterns with Arrow, Raise DSL, and coroutine-based effects
From nwnpx claudepluginhub nwave-ai/nwave --plugin nwThis skill uses the workspace's default tool permissions.
FP in Kotlin -- Functional Software Crafter Skill
Cross-references: fp-principles | fp-domain-modeling | pbt-jvm
When to Choose Kotlin
- Best for: gentlest FP onboarding for Java teams | Android | coroutines | pragmatic FP with great IDE support
- Not ideal for: teams wanting compiler-enforced purity | full effect systems | higher-kinded type abstractions
[STARTER] Quick Setup
mkdir order-service && cd order-service
gradle init --type kotlin-application --dsl kotlin
# Add arrow-core, arrow-fx-coroutines, kotest-property to build.gradle.kts
./gradlew build && ./gradlew test
[STARTER] Type System for Domain Modeling
Choice Types (Sealed Hierarchies)
sealed interface PaymentMethod {
data class CreditCard(val cardNumber: String, val expiryDate: String) : PaymentMethod
data class BankTransfer(val accountNumber: String) : PaymentMethod
data object Cash : PaymentMethod
}
Exhaustive when expressions ensure all cases handled.
Record Types and Domain Wrappers
data class Customer(
val customerId: CustomerId,
val customerName: CustomerName,
val customerEmail: EmailAddress
)
@JvmInline
value class OrderId(val value: Int)
@JvmInline
value class EmailAddress private constructor(val value: String) {
companion object {
fun from(raw: String): Either<ValidationError, EmailAddress> =
if ("@" in raw) EmailAddress(raw).right()
else InvalidEmail(raw).left()
}
}
Value classes inlined by compiler -- zero allocation overhead.
[INTERMEDIATE] Validated Construction with Raise DSL
import arrow.core.raise.Raise
import arrow.core.raise.ensure
context(Raise<ValidationError>)
fun validateEmail(raw: String): EmailAddress {
ensure(raw.contains("@")) { InvalidEmail(raw) }
return EmailAddress.unsafeCreate(raw)
}
[INTERMEDIATE] Composition Style
Scope Functions as Lightweight Pipelines
val result = rawOrder
.let(::validateOrder)
.map(::priceOrder)
.map(::confirmOrder)
Arrow Raise DSL (Direct-Style Typed Errors)
import arrow.core.raise.either
fun placeOrder(raw: RawOrder): Either<OrderError, Confirmation> = either {
val validated = validateOrder(raw).bind()
val priced = priceOrder(validated).bind()
confirmOrder(priced).bind()
}
Looks imperative, behaves functionally. bind() short-circuits on Left.
Accumulating Errors
import arrow.core.raise.zipOrAccumulate
fun validateCustomer(raw: RawCustomer): Either<NonEmptyList<ValidationError>, Customer> = either {
zipOrAccumulate(
{ validateName(raw.name) },
{ validateEmail(raw.email) },
{ validateAddress(raw.address) }
) { name, email, address -> Customer(name, email, address) }
}
[INTERMEDIATE] Effect Management
Kotlin is impure by default. suspend marks functions performing side effects or async work.
Pure Core / Imperative Shell with Coroutines
// Pure domain logic (no suspend, no side effects)
object OrderDomain {
fun calculateDiscount(order: Order): Discount =
if (order.lines.size > 10) Discount(0.1) else Discount(0.0)
}
// Imperative shell (suspend = side effects)
class OrderService(
private val orderRepo: OrderRepository,
private val pricingService: PricingService
) {
suspend fun placeOrder(raw: RawOrder): Either<OrderError, Confirmation> = either {
val validated = OrderDomain.validateOrder(raw).bind()
val priced = pricingService.price(validated).bind()
orderRepo.save(priced)
Confirmation(priced.orderId)
}
}
[ADVANCED] Hexagonal Architecture with Interfaces
// Port: interface defining capability
interface OrderRepository {
suspend fun findOrder(id: OrderId): Order?
suspend fun saveOrder(order: Order)
}
// Adapter: concrete implementation
class PostgresOrderRepository(private val db: Database) : OrderRepository {
override suspend fun findOrder(id: OrderId): Order? =
db.query("SELECT * FROM orders WHERE id = ?", id.value)
override suspend fun saveOrder(order: Order) =
db.execute("INSERT INTO orders ...", order)
}
DI: constructor injection via Koin, Dagger/Hilt, or manual wiring.
[INTERMEDIATE] Testing
Frameworks: Kotest (test framework + PBT) | jqwik (PBT on JUnit 5) | MockK (coroutine-aware mocking). See pbt-jvm for detailed PBT patterns.
Property Test (Kotest)
import io.kotest.core.spec.style.FunSpec
import io.kotest.property.forAll
class OrderSpec : FunSpec({
test("serialization round-trips") {
forAll(orderArb) { order -> deserialize(serialize(order)) == order.right() }
}
test("validated orders have positive totals") {
forAll(rawOrderArb) { raw ->
when (val result = validateOrder(raw)) {
is Either.Left -> true
is Either.Right -> result.value.total.value > 0
}
}
}
})
Custom Generator
val emailArb: Arb<EmailAddress> = arbitrary {
val user = Arb.string(minSize = 1, maxSize = 10, codepoints = Codepoint.az()).bind()
val domain = Arb.string(minSize = 1, maxSize = 8, codepoints = Codepoint.az()).bind()
EmailAddress.unsafeCreate("$user@$domain.com")
}
[ADVANCED] Idiomatic Patterns
Null Safety as Option Alternative
fun findCustomer(id: CustomerId): Customer? = ...
val email = findCustomer(id)?.customerEmail?.value
// Use Either/Raise when you need error context
context(Raise<CustomerError>)
fun getCustomer(id: CustomerId): Customer =
findCustomer(id) ?: raise(CustomerNotFound(id))
Arrow Maturity Caveat
Arrow is capable with active development, but carries honest risks:
- Context receivers/parameters (Raise DSL relies on) are still experimental in Kotlin. Feature renamed and redesigned multiple times.
- Arrow's API changed significantly between 1.x and 2.x, requiring substantial rewrites.
- Smaller contributor base than ZIO or Cats Effect.
- For long-term stability needs, evaluate whether Arrow's API surface will remain stable for your project's lifetime.
Common Pitfalls
- Arrow API instability: Pin versions carefully. Read migration guides before major upgrades.
- No higher-kinded types: Use concrete types or Arrow's Raise DSL as abstraction mechanism.
- FP is library-dependent: Without Arrow, Kotlin FP limited to null safety, sealed classes, and extensions.
- Coroutine complexity:
CoroutineScope,Dispatcher,SupervisorJobinteractions are subtle. Test withrunTest.