FP in Kotlin -- Functional Software Crafter Skill
SkillDev toolsKotlin language-specific patterns with Arrow, Raise DSL, and coroutine-based effects
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the FP in Kotlin -- Functional Software Crafter Skill skill
What this skill tells your AI
The instructions your AI receives, as published by nwave-ai/nwave in nWave/skills/nw-fp-kotlin/SKILL.md and read by ahel’s review.
Cross-references: fp-principles | fp-domain-modeling | property-based-testing
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 property-based-testing 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.
Signals
- GitHub stars
- 610
- Forks
- 64
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
nw-fp-kotlin- Source
- github.com/nwave-ai/nwave