NWave nw-fp-kotlin
Kotlin language-specific patterns with Arrow, Raise DSL, and coroutine-based effects
install
source · Clone the upstream repo
git clone https://github.com/nWave-ai/nWave
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/nWave-ai/nWave "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/nw/skills/nw-fp-kotlin" ~/.claude/skills/nwave-ai-nwave-nw-fp-kotlin-8163e1 && rm -rf "$T"
manifest:
plugins/nw/skills/nw-fp-kotlin/SKILL.mdsource content
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
interactions are subtle. Test withSupervisorJob
.runTest