State & Resources
λÆS provides two complementary effects for managing mutable data and external resources: the State effect for functional stateful computations, and the Resource effect for guaranteed cleanup of acquired resources.
State Effect
Section titled “State Effect”The State[S] effect enables stateful computations in a purely functional manner. It provides operations to get, set, update, and use state values within a controlled scope, allowing you to work with mutable state without compromising functional programming principles.
Overview
Section titled “Overview”The State effect manages a single piece of state of type S throughout a computation. All state operations are performed within the context of a State.run block, which provides isolation and ensures the state is properly managed.
Note: The State effect is not thread-safe. Use appropriate synchronization mechanisms when accessing state from multiple threads.
Getting and Setting State
Section titled “Getting and Setting State”import in.rcard.yaes.State.*
val (finalState, result) = State.run(0) { val current = State.get[Int] State.set(current + 5) State.get[Int]}// finalState = 5, result = 5Updating State
Section titled “Updating State”The update operation applies a transformation function to the current state and returns the new value:
import in.rcard.yaes.State.*
val (finalState, result) = State.run(10) { val doubled = State.update[Int](_ * 2) val tripled = State.update[Int](_ * 3) tripled}// finalState = 60, result = 60Reading Without Modification
Section titled “Reading Without Modification”Use State.use when you only need to derive a value from state without changing it:
import in.rcard.yaes.State.*
case class User(name: String, age: Int)
val (finalState, nameLength) = State.run(User("Alice", 30)) { State.use[User, Int](_.name.length)}// finalState = User("Alice", 30), nameLength = 5Advanced: Counter with Operations
Section titled “Advanced: Counter with Operations”You can build composable state operations using context parameters:
import in.rcard.yaes.State.*
def increment(using State[Int]): Int = State.update(_ + 1)def decrement(using State[Int]): Int = State.update(_ - 1)def multiply(factor: Int)(using State[Int]): Int = State.update(_ * factor)
val (finalState, result) = State.run(5) { increment increment multiply(3) decrement}// finalState = 20, result = 20Advanced: Complex State Types
Section titled “Advanced: Complex State Types”State works well with case classes for modeling domain objects:
import in.rcard.yaes.State.*
case class GameState(score: Int, lives: Int, level: Int)
def addScore(points: Int)(using State[GameState]): GameState = State.update(state => state.copy(score = state.score + points))
def loseLife(using State[GameState]): GameState = State.update(state => state.copy(lives = state.lives - 1))
def nextLevel(using State[GameState]): GameState = State.update(state => state.copy(level = state.level + 1))
val (finalState, _) = State.run(GameState(0, 3, 1)) { addScore(100) addScore(250) loseLife nextLevel State.get[GameState]}// finalState = GameState(350, 2, 2)Combining State with Other Effects
Section titled “Combining State with Other Effects”State composes naturally with other λÆS effects:
import in.rcard.yaes.State.*import in.rcard.yaes.Random.*
def randomWalk(steps: Int)(using State[Int], Random): Int = { if (steps <= 0) State.get[Int] else { val direction = if (Random.nextBoolean) 1 else -1 State.update(_ + direction) randomWalk(steps - 1) }}
val (finalPosition, result) = State.run(0) { Random.run { randomWalk(10) }}State API Reference
Section titled “State API Reference”| Operation | Signature | Description |
|---|---|---|
State.get | State[S] ?=> S | Retrieve current state value |
State.set | (S) => State[S] ?=> S | Set new value, return previous |
State.update | (S => S) => State[S] ?=> S | Transform state, return new value |
State.use | (S => A) => State[S] ?=> A | Read-only projection, state unchanged |
State.run | (S) => (State[S] ?=> A) => (S, A) | Run computation with initial state |
Thread Safety
Section titled “Thread Safety”The State effect is not thread-safe. For concurrent scenarios:
- Use separate
State.runblocks per thread — each gets isolated state - Implement synchronization or use atomic types if shared state is required
// Safe: each thread has its own isolated stateval thread1Result = Future { State.run(0) { /* stateful computation */ }}
val thread2Result = Future { State.run(0) { /* stateful computation */ }}Resource Effect
Section titled “Resource Effect”The Resource effect provides automatic resource management with guaranteed cleanup. It ensures that all acquired resources are properly released in LIFO (Last In, First Out) order, even when exceptions occur.
This is essential for managing files, database connections, network connections, and any other resource that needs explicit cleanup.
Auto-Closeable Resources
Section titled “Auto-Closeable Resources”For resources implementing java.io.Closeable, use Resource.acquire:
import in.rcard.yaes.Resource.*import java.io.{FileInputStream, FileOutputStream}
def copyFile(source: String, target: String)(using Resource): Unit = { val input = Resource.acquire(new FileInputStream(source)) val output = Resource.acquire(new FileOutputStream(target))
// Copy file contents val buffer = new Array[Byte](1024) var bytesRead = input.read(buffer) while (bytesRead != -1) { output.write(buffer, 0, bytesRead) bytesRead = input.read(buffer) } // Both streams automatically closed here}Custom Acquisition and Release
Section titled “Custom Acquisition and Release”Use Resource.install for resources with custom cleanup logic:
import in.rcard.yaes.Resource.*
def processWithConnection()(using Resource): String = { val connection = Resource.install(openDatabaseConnection()) { conn => conn.close() println("Database connection closed") }
// Use connection safely — cleanup is guaranteed connection.executeQuery("SELECT * FROM users")}Registering Cleanup Actions
Section titled “Registering Cleanup Actions”Register standalone cleanup actions with Resource.ensuring:
import in.rcard.yaes.Resource.*
def processData()(using Resource): Unit = { Resource.ensuring { println("Processing completed") }
Resource.ensuring { println("Cleanup temporary files") }
// Main processing logic here // Cleanup actions run in reverse order (LIFO)}Running Resource-Managed Code
Section titled “Running Resource-Managed Code”Wrap resource-using code in Resource.run to trigger cleanup:
import in.rcard.yaes.Resource.*
val result = Resource.run { copyFile("source.txt", "target.txt") processWithConnection()}// All resources automatically cleaned up hereLIFO Cleanup Order
Section titled “LIFO Cleanup Order”Resources are always released in the reverse order they were acquired:
import in.rcard.yaes.Resource.*
def nestedResourceExample()(using Resource): Unit = { val outer = Resource.acquire(new FileInputStream("outer.txt")) println("Outer resource acquired")
Resource.ensuring { println("Outer cleanup") }
val inner = Resource.acquire(new FileInputStream("inner.txt")) println("Inner resource acquired")
Resource.ensuring { println("Inner cleanup") }}
Resource.run { nestedResourceExample() }// Output:// Outer resource acquired// Inner resource acquired// Inner cleanup// Outer cleanup ← LIFO: last acquired, first releasedError Safety
Section titled “Error Safety”Resources are cleaned up even when exceptions or raised errors occur:
import in.rcard.yaes.Resource.*import in.rcard.yaes.Raise.*
def riskyOperation()(using Resource, Raise[String]): String = { val resource = Resource.acquire(new FileInputStream("data.txt"))
if (Math.random() > 0.5) { Raise.raise("Random failure!") }
"Success" // File is closed regardless of whether an error was raised}Advanced: Multiple Resource Types
Section titled “Advanced: Multiple Resource Types”Resources of different types compose cleanly in a single scope:
import in.rcard.yaes.Resource.*import java.io.*import java.net.*
def downloadAndProcess(url: String, outputFile: String)(using Resource): Unit = { // Network connection with custom disconnect logic val connection = Resource.install(new URL(url).openConnection()) { conn => conn.asInstanceOf[HttpURLConnection].disconnect() }
// Input and output streams (auto-closeable) val input = Resource.acquire(connection.getInputStream()) val output = Resource.acquire(new FileOutputStream(outputFile))
// Completion notification Resource.ensuring { println(s"Download of $url completed") }
input.transferTo(output)}Resource API Summary
Section titled “Resource API Summary”| Method | Description |
|---|---|
Resource.acquire(r) | Acquire a Closeable resource; auto-close on scope exit |
Resource.install(r)(cleanup) | Acquire any resource with a custom cleanup function |
Resource.ensuring(block) | Register a cleanup action to run on scope exit |
Resource.run { ... } | Execute a block and release all acquired resources |
Key guarantees:
- Cleanup always runs, even on exceptions or raised errors
- Resources are released in LIFO order
- Composable with all other λÆS effects
With State and Resource in your toolkit, you have everything you need for managing mutable data and external dependencies safely. Next, we’ll look at the most powerful data-flow primitives: Streams & Channels.