Skip to content

Functional Programming at Arda

This document summarizes the functional programming (FP) patterns and techniques used across the Arda codebase, specifically within common-module, operations, and arda-frontend-app.

The codebase demonstrates a strong commitment to functional paradigms, particularly Railway Oriented Programming for error handling and Monadic composition for side effects (I/O).

For implementation code paths and “use when” guidance for specific patterns, see Implementation Patterns.


1. Railway Oriented Programming (ROP) [BE]

Section titled “1. Railway Oriented Programming (ROP) [BE]”

The most prevalent pattern is the use of the Result type to model success and failure, ensuring errors are handled explicitly as values rather than exceptions.

  • Core Abstraction: kotlin.Result<T>
  • Extensions: cards.arda.common.lib.lang.ResultExt.kt
  • Key Operations:
    • flatMap: Chains operations that both return Result, allowing short-circuiting on failure
    • mapCatching: Wraps throwing code into a Result
    • collectAll: Transforms Iterable<Result<T>> into Result<List<T>> (all success or first failure)
    • unitify: Discards the value, keeping the success/failure status
// Chaining validations
fun validate(input: Input): Result<Unit> {
return validateFormat(input)
.flatMap { validateBusinessRule(it) }
.flatMap { validateConsistency(it) }
}
// Aggregating results from a list
val validItems: Result<List<Item>> = rawItems.map { validate(it) }.collectAll()

This pattern is used throughout domain, service, and persistence code. For API-level error response conventions see Exception Handling.

Database interactions are modeled as lazy, suspendable computations that return a Result. This separates the definition of an operation from its execution, allowing complex transaction scripts to be composed from smaller, reusable blocks before any I/O occurs.

  • Core type: typealias DBIO<R> = suspend () -> Result<R>
  • Location: cards.arda.common.lib.persistence.types.DBIO.kt
  1. Lazy: A DBIO is just a function. Creating it does not execute the operation — it must be invoked to run.
  2. Side-Effecting: Encapsulates I/O that can be executed in a transactional context using any Coroutine Executor.
  3. Fail-Safe: Wraps the result in Result<R>, forcing explicit failure handling.
  4. Suspendable: Supports Kotlin coroutines for non-blocking I/O.
// map: transform the successful result
val getDoubleCount: DBIO<Int> = getCount.map { it * 2 }
// liftMap / flatMap: sequential dependency
val userPosts: DBIO<List<Post>> = findUser.liftMap { user -> findPosts(user) }
// then: sequence, ignoring first result
val loggedGetData: DBIO<Data> = logAccess.then(getData)
// both: zip two operations
val combined: DBIO<Pair<Int, String>> = op1.both(op2)
// validate and chain
val op: DBIO<Entity> = validate(input).lift().then(createEntity(input))

DBIO operations are transaction-agnostic by default. They can be lifted into a transactional context using TDBIO:

typealias TDBIO<R> = suspend Transaction.() -> Result<R>
  • transactional(): Converts DBIO<T> to TDBIO<T>
  • io(tx): Runs a TDBIO<T> using a specific Exposed Transaction

Compose multiple DBIO operations, then execute them all within a single transaction at the Service or Endpoint layer.

fun createWithDependent(parentPayload: Parent, childPayload: Child): DBIO<Pair<Parent, Child>> {
val createParentOp = parentUniverse.create(parentPayload, ...)
return createParentOp.liftMap { createdParent ->
val childWithRef = childPayload.copy(parentId = createdParent.eId)
childUniverse.create(childWithRef, ...).map { createdChild ->
createdParent to createdChild
}
}
}
interface Universe<EP, M> {
suspend fun create(...): DBIO<BitemporalEntity<EP, M>>
suspend fun read(...): DBIO<BitemporalEntity<EP, M>?>
// ... other operations
}

Calling universe.create(...) does not write to the database. It returns a DBIO “program” that must be executed to apply the change.

The domain model relies on immutable data structures.

  • Technique: data class with val properties
  • Mutation: Via the copy() method — creates a new instance with specific fields modified
  • Benefit: Thread safety and predictable state flow
// "Mutating" an immutable state
val resolvedDefaultSupply = when(payload.defaultSupply) {
null -> primaryName ?: secondaryName
else -> payload.defaultSupply
}
return payload.copy(
primarySupply = payload.primarySupply?.copy(name = primaryName),
defaultSupply = resolvedDefaultSupply
)

The codebase implements a Scala-like PartialFunction abstraction, useful for defining logic that applies only to a subset of inputs.

  • Location: cards.arda.lang.PartialFunction.kt
  • Features: isDefinedAt, orElse (composition)
  • Usage: Event handlers or state transitions where a handler only deals with specific event types

The StateEngine library models state transitions as a composition of functions. States and transitions are declared with guard predicates and entry/exit actions; the engine generates executors from this declaration.

  • Core type: typealias Operation<...> = suspend CTX?.(...) -> Result<RESULT>
  • Location: cards.arda.common.lib.service.stateengine
  • Code: /common-module/lib/src/main/kotlin/cards/arda/common/lib/service/stateengine/StateEngine.kt

Functions are first-class citizens and are often passed as arguments to parameterize behavior.

Example: validateSupply accepts iterables of validation functions or lambdas to customize strictness (checkAndVerify vs checkLaxConsistency).


UI logic is encapsulated in pure functional components that render based on props and hooks. State is local and managed via standard React hooks (useState, useReducer).

Standard JavaScript array methods (map, filter, reduce) are preferred over imperative loops for rendering lists and transforming data.


The code strictly separates Data (Configuration) from Behavior (Constructs):

  • Interfaces as Contracts: Inputs (Props) and Outputs (Built, ExportValues) are defined as readonly interfaces, enforcing immutability
  • Pure Transformation Functions: Complex logic is extracted into pure functions that transform data without side effects

Property mutation is avoided:

  • All properties in Props and Built interfaces are readonly
  • Helper functions like safeSubstitute return new strings rather than mutating buffers
  • Static Validation: validateProps methods are static pure functions that return Error[]
  • Logic Extraction: Heavy logic is moved out of constructors into private methods or external utilities

ConceptBackend (Kotlin)Frontend (TypeScript)
Error HandlingResult<T> Monad (Railway Oriented)Try/Catch, Promise Chains
Side EffectsDBIO (IO Monad), CoroutinesuseEffect, Event Handlers
Immutabilitydata class, copy(), valconst, Spread operator ...
CollectionsExtension functions (collectAll, map)Array methods (map, filter)
StateStateEngine (Functional State Machine)useState, useReducer