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

TypeOne-linerGuide
Option<T>A value that may or may not be presentOption guide
Result<V, E>Either a success value or a typed domain errorResult guide
Try<V>A computation that may throw, turned into a valueTry guide
Validated<E, A>Like Result, but accumulates all errors instead of oneValidated guide
Either<L, R>A neutral disjoint union — neither side means failureEither guide
Lazy<T>A value computed at most once, cached after first accessLazy guide
Tuple2/3/4Typed heterogeneous groupings without a named classTuples guide
NonEmptyList<T>A list with at least one element, enforced at compile timeNEL guide
Accumulator<E, A>A value paired with a side-channel accumulation (log, metrics, audit trail)Accumulator guide
Checked interfacesCheckedFunction, TriFunction, QuadFunction, and moreChecked interfaces guide

How to read this guide

Each type page follows the same structure:

  1. What it is — the problem it solves and when to reach for it
  2. Creating instances — factory methods and constructors
  3. Accessing / extracting values — safe and unsafe extraction
  4. Transformingmap, flatMap, filter, and friends
  5. Interoperability — conversion to other types
  6. Common pitfalls — the mistakes made most often
  7. 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.