Introduction
What is dmx-fun?
dmx-fun is a functional programming toolkit for Java 25+. It fills the gap between plain Java and full FP frameworks by providing a small set of precisely scoped types that eliminate the most common sources of runtime surprises: null references, swallowed exceptions, and silent partial failures.
The library depends on JSpecify for its null-safety
annotations (@NullMarked). An optional fun-jackson module adds JSON
serialization for all types.
Core philosophy
Make illegal states unrepresentable.
Types like Option<T>, Result<V,E>, and NonEmptyList<T> encode constraints
directly in the type system. A method returning Option<T> cannot accidentally
return null; a method returning NonEmptyList<T> cannot return an empty list.
The compiler enforces the contract, not a runtime check.
Explicit over implicit.
Absent values, domain errors, and thrown exceptions are all surfaced as first-class
values. Nothing is hidden in a null return, a swallowed catch block, or an
unchecked exception that only surfaces in production.
Composable by design.
Every type provides map, flatMap, and conversion methods to the other types.
A Try converts to Result; a Result converts to Option; a Validated
converts to Result. Pipelines stay clean because the plumbing is built in.
Pattern matching as the primary API.
All types are sealed interfaces with record variants (Option.Some/Option.None,
Try.Success/Try.Failure, Result.Ok/Result.Err, etc.). Java 21+ exhaustive
switch expressions are the idiomatic way to branch on them — compiler-checked,
without instanceof boilerplate, and with destructuring built in.
The type landscape
| Type | One-liner | Guide |
|---|---|---|
Option<T> | A value that may or may not be present | Option guide |
Result<V, E> | Either a success value or a typed domain error | Result guide |
Try<V> | A computation that may throw, turned into a value | Try guide |
Validated<E, A> | Like Result, but accumulates all errors instead of one | Validated guide |
Either<L, R> | A neutral disjoint union — neither side means failure | Either guide |
Lazy<T> | A value computed at most once, cached after first access | Lazy guide |
Tuple2/3/4 | Typed heterogeneous groupings without a named class | Tuples guide |
NonEmptyList<T> | A list with at least one element, enforced at compile time | NEL guide |
Accumulator<E, A> | A value paired with a side-channel accumulation (log, metrics, audit trail) | Accumulator guide |
| Checked interfaces | CheckedFunction, TriFunction, QuadFunction, and more | Checked interfaces guide |
How to read this guide
Each type page follows the same structure:
- What it is — the problem it solves and when to reach for it
- Creating instances — factory methods and constructors
- Accessing / extracting values — safe and unsafe extraction
- Transforming —
map,flatMap,filter, and friends - Interoperability — conversion to other types
- Common pitfalls — the mistakes made most often
- Real-world example — a concrete, idiomatic usage
Types interoperate — the Combining Types page covers the full conversion matrix and cross-type composition patterns.
For performance expectations and design trade-offs, see the Performance page.
A taste of composition
The snippet below registers a user by combining three types, each doing what
it does best: Try for exception handling, Validated for collecting all
validation errors, and Result for the typed outcome returned to the caller.
// End-to-end registration flow combining Try, Validated, and Result.//// 1. Try — wrap the HTTP body parse (may throw)// 2. Validated — accumulate ALL field validation errors// 3. Result — persist; the DB layer speaks Result<User, DbError>
public Result<User, RegistrationError> register(HttpRequest req) {
// Step 1: parse the request body — any parse exception becomes a Failure Try<RawInput> parsed = Try.of(() -> bodyParser.parse(req));
if (parsed instanceof Try.Failure<RawInput>(var ex)) { return Result.err(RegistrationError.invalidRequest(ex.getMessage())); }
RawInput input = parsed.get();
// Step 2: validate all fields — collect every error, not just the first Validated<NonEmptyList<String>, RegistrationForm> validation = validateEmail(input.email()) .combine(validatePassword(input.password()), NonEmptyList::concat, RegistrationForm::new);
// Step 3: convert and persist return switch (validation) { case Validated.Valid<?, RegistrationForm>(var form) -> userRepository.save(form) // Result<User, DbError> .mapErr(RegistrationError::dbFailure); // unify error type case Validated.Invalid<NonEmptyList<String>, ?>(var errors) -> Result.err(RegistrationError.validationFailed(errors.toList())); };}The types stay in their natural roles throughout and convert only at the boundary where the next layer demands a different type. That is the core pattern of dmx-fun: choose the right type for each concern, convert at the seam.