Skip to content

thma/jiffy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jiffy - Algebraic Effects for Java

jiffy

Jiffy is a lightweight library that brings algebraic effects to Java with compile-time effect checking through annotations.

Features

  • 🔍 Compile-time effect checking - Catch missing effect declarations during compilation
  • 📝 Annotation-based - Use familiar Java annotations to declare effects
  • 🎯 Type-safe - Effects are visible in method signatures
  • 🔧 Extensible - Easy to add new effects and handlers
  • 🏗️ Spring-friendly - Integrates well with Spring Boot applications
  • Minimal overhead - Efficient runtime with direct handler dispatch
  • 🧪 Multiple execution modes - Sync, async, traced, and dry-run
  • 🧵 Structured concurrency - Uses Java 25 virtual threads and StructuredTaskScope
  • 🛡️ Stack-safe - Handles arbitrarily deep effect chains without stack overflow

Demo Project

This demo shows how to use jiffy in a SpringBoot application. It also has a nice introduction to core concepts used:

https://github.com/thma/jiffy-clean-architecture/

Quick Start

Define an Effect

public sealed interface LogEffect extends Effect<Void> {
    record Info(String message) implements LogEffect {}
    record Error(String message) implements LogEffect {}
}

Use Effects in Your Code

import org.jiffy.annotations.Uses;
import org.jiffy.core.Eff;

public class UserService {

    @Uses({LogEffect.class, DatabaseEffect.class})
    public Eff<User> findUser(Long id) {
        return Eff.perform(new LogEffect.Info("Finding user " + id))
            .flatMap(ignored ->
                Eff.perform(new DatabaseEffect.Query("SELECT * FROM users WHERE id = " + id))
            )
            .map(result -> parseUser(result));
    }
}

Handle Effects

public class LogHandler implements EffectHandler<LogEffect> {
    @Override
    public <T> T handle(LogEffect effect) {
        switch (effect) {
            case LogEffect.Info(var message) -> System.out.println("[INFO] " + message);
            case LogEffect.Error(var message) -> System.err.println("[ERROR] " + message);
        }
        return null;  // LogEffect returns Void
    }
}

Run with Runtime

EffectRuntime runtime = EffectRuntime.builder()
    .withHandler(LogEffect.class, new LogHandler())
    .withHandler(DatabaseEffect.class, new DatabaseHandler())
    .build();

// Execute the program
User user = runtime.run(findUser(123L));

Compile-Time Checking

The annotation processor validates that all effects used in a method are declared:

@Uses({LogEffect.class})  // Missing DatabaseEffect!
public Eff<User> findUser(Long id) {
    return Eff.perform(new DatabaseEffect.Query("..."))  // Compile error!
        .map(this::parseUser);
}

Installation

Maven

<dependency>
    <groupId>org.jiffy</groupId>
    <artifactId>jiffy</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

Gradle

implementation 'org.jiffy:jiffy:1.0.0-SNAPSHOT'

Core Concepts

Effects

Effects represent side effects as data. They extend the Effect<T> interface where T is the return type.

Eff Monad

Eff<T> is a monadic type that represents a computation that may perform effects and produce a value of type T. It is a pure data structure describing what to do, not how to do it.

Effect Handlers

Handlers interpret effects. They implement EffectHandler<E> where E is the effect type.

EffectRuntime

The runtime holds registered handlers and provides methods to execute Eff programs:

EffectRuntime runtime = EffectRuntime.builder()
    .withHandler(LogEffect.class, logHandler)
    .withHandler(DatabaseEffect.class, dbHandler)
    .build();

Annotations

  • @Uses - Declares which effects a method may use
  • @Pure - Marks methods as effect-free

Execution Modes

Jiffy provides multiple ways to execute effect programs:

Synchronous Execution

// Direct execution
User user = runtime.run(findUser(123L));

Asynchronous Execution

Jiffy uses Java 25's virtual threads and StructuredTaskScope for efficient async execution:

// Returns immediately with a StructuredFuture (runs on virtual thread)
StructuredFuture<User> future = runtime.runAsync(findUser(123L));

// Block and wait for result
User user = future.join();

// Wait with timeout
User user = future.join(5, TimeUnit.SECONDS);

// Compose async operations
StructuredFuture<Report> report = runtime.runAsync(fetchUser(id))
    .flatMap(user -> runtime.runAsync(fetchOrders(user.id()))
    .map(orders -> generateReport(user, orders)));

// Interop with CompletableFuture when needed
CompletableFuture<User> cf = future.toCompletableFuture();

Traced Execution

Capture all effects performed during execution for debugging, testing, or auditing:

Traced<User> traced = runtime.runTraced(findUser(123L));

// Access the result
User user = traced.result();

// Inspect the effect log
System.out.println("Effects performed: " + traced.effectCount());
traced.effectLog().forEach(System.out::println);

// Query specific effect types
if (traced.hasEffect(DatabaseEffect.Query.class)) {
    List<DatabaseEffect.Query> queries = traced.getEffects(DatabaseEffect.Query.class);
    System.out.println("Database queries: " + queries.size());
}

Dry Run (Static Analysis)

Inspect effects without executing them:

List<Effect<?>> effects = runtime.dryRun(findUser(123L));
System.out.println("This program would perform: " + effects);

Advanced Features

For Comprehensions

Scala-style for comprehensions for composing multiple effects:

Eff<CustomerReport> report = Eff.For(
    perform(new LogEffect.Info("Generating report")),
    perform(new DatabaseEffect.FindCustomer(customerId)),
    perform(new DatabaseEffect.FindOrders(customerId))
).yield((log, customer, orders) ->
    new CustomerReport(customer, orders)
);

Parallel Effects

Run effects concurrently using StructuredTaskScope under the hood:

Eff.parallel(
    fetchUserData(userId),
    fetchUserOrders(userId)
).map(pair -> new UserProfile(pair.getFirst(), pair.getSecond()));

Effect Recovery

// Simple recovery with a fallback value
fetchData()
    .recover(error -> defaultData());

// Recovery with logging (using recoverWith for proper effect handling)
fetchData()
    .recoverWith(error ->
        Eff.perform(new LogEffect.Error("Failed: " + error.getMessage()))
            .map(v -> defaultData())
    );

Sequential Composition

Eff.andThen(
    validateInput(data),
    saveToDatabase(data),
    notifyUser(userId)
);

Stack-Safe Execution

Jiffy's interpreter uses an iterative algorithm with an explicit continuation stack, making it safe for arbitrarily deep effect chains:

// This works fine - no StackOverflowError!
Eff<Integer> program = Eff.pure(0);
for (int i = 0; i < 100_000; i++) {
    program = program.flatMap(n -> Eff.pure(n + 1));
}
Integer result = runtime.run(program);  // Returns 100000

This is essential for:

  • Stream processing - Processing large collections with flatMap
  • Recursive patterns - Paginated API calls, tree traversals
  • Long-running workflows - Business processes with many steps

The implementation avoids Java's lack of tail-call optimization by converting recursion to iteration internally.

Architecture

Jiffy follows a clean separation between effect description and interpretation:

┌─────────────────────────────────────────────────────────┐
│                    Your Application                      │
│                                                          │
│  ┌──────────────┐    ┌──────────────┐    ┌───────────┐  │
│  │   Effects    │    │  Eff<T>      │    │ @Uses     │  │
│  │   (Data)     │───▶│  (Program)   │◀───│ @Pure     │  │
│  └──────────────┘    └──────────────┘    └───────────┘  │
│                             │                            │
└─────────────────────────────│────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────┐
│                    EffectRuntime                         │
│                                                          │
│  ┌──────────────┐    ┌──────────────┐    ┌───────────┐  │
│  │  Handlers    │    │ Interpreter  │    │ Execution │  │
│  │              │◀───│ (stack-safe  │───▶│ Modes     │  │
│  │              │    │  iterative)  │    │           │  │
│  └──────────────┘    └──────────────┘    └───────────┘  │
│                                                          │
│         run() | runAsync() | runTraced() | dryRun()      │
└─────────────────────────────────────────────────────────┘

Building from Source

git clone https://github.com/yourusername/jiffy.git
cd jiffy
mvn clean install

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

License

This project is licensed under the Apache 2.0 License - see the LICENSE file for details.

Acknowledgments

  • Inspired by algebraic effects in Haskell and OCaml
  • Similar projects: Jeff, Roux
  • Built for the Java community with ❤️

About

algebraic effects for Java

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages