Version 0.0.12 is out. This release rounds out the core API with Lazy<T>,
CompletableFuture adapters, zip combinators for three and four containers, and
Try.flatMapError. Under the hood, sequence / traverse have been rewritten with
Stream Gatherers and the internal architecture was unified through a shared Bicontainer
interface. Here is everything that changed.
Lazy<T> — deferred, memoized evaluation
Lazy<T> wraps a computation that is evaluated at most once, on first access, and then
cached for all subsequent calls.
Lazy<Config> config = Lazy.of(() -> Config.loadFromDisk(path));
// Nothing runs yet.Config c = config.get(); // evaluates now, result cachedConfig c2 = config.get(); // returns cached result — supplier NOT called againIf the supplier throws, the exception is captured and rethrown on every call — the at-most-once contract is honoured even for failures.
Lazy<Connection> conn = Lazy.of(() -> { throw new IOException("unreachable host");});
conn.get(); // throws IOExceptionconn.get(); // throws the same IOException — supplier was never called a second timemap without forcing evaluation
Lazy<String> appName = Lazy.of(() -> Config.load()).map(Config::appName);// Config.load() has NOT run yetCompletableFuture bridge
CompletableFuture<User> future = userService.fetchAsync(id);Lazy<Try<User>> lazyUser = Lazy.fromFuture(future);// future result wrapped in Try<User> on first accessCompletableFuture adapters for Try and Result
Bridging between async code and functional types is now first-class.
Try adapters
// Wrap an in-flight future as Try<V>CompletableFuture<Order> future = orderService.placeAsync(req);Try<Order> result = Try.fromFuture(future);
// CancellationException and CompletionException are unwrapped to their causeresult.fold( order -> "Placed: " + order.id(), ex -> "Failed: " + ex.getMessage());
// Convert Try back to a CompletableFutureCompletableFuture<Order> f = result.toFuture();// — already completed; Failure becomes exceptionally-completedResult adapters
Result<Order, Throwable> result = Result.fromFuture(orderService.placeAsync(req));
result.fold( order -> render(order), err -> renderError(err));zip3 and zip4
Combine three or four independent containers into a single result with zip3 / zip4.
All containers must be present / successful; otherwise the first absent or failed value is
propagated.
zip3
Option<String> name = Option.some("Alice");Option<Integer> age = Option.some(30);Option<String> country = Option.some("MX");
// Combine into a Tuple3Option<Tuple3<String, Integer, String>> t = Option.zip3(name, age, country);// Some(("Alice", 30, "MX"))
// Combine with a custom functionOption<String> label = Option.zipWith3(name, age, country, (n, a, c) -> n + " (" + a + ") from " + c);// Some("Alice (30) from MX")The same API is available on Result and Try:
Result<Tuple3<User, Profile, Settings>, String> data = Result.zip3(loadUser(id), loadProfile(id), loadSettings(id));zip4
Try<Tuple4<String, Integer, Boolean, Double>> t = Try.zip4( Try.of(() -> name()), Try.of(() -> age()), Try.of(() -> active()), Try.of(() -> score()) );
// Collapse with QuadFunctionTry<String> summary = Try.zipWith4( Try.of(() -> name()), Try.of(() -> age()), Try.of(() -> active()), Try.of(() -> score()), (n, a, act, s) -> n + " | " + a + " | " + act + " | " + s);Try.flatMapError — recovery on the failure channel
flatMapError is the dual of flatMap: it operates on Failure values, allowing you to
attempt recovery with another fallible computation.
Try<Config> config = Try.of(() -> loadFromFile(path)) .flatMapError(ex -> Try.of(() -> loadFromClasspath(path))) .flatMapError(ex -> Try.success(Config.defaults()));- If this is a
Success, it is returned unchanged — the mapper is never called. - If the mapper throws or returns
null, the exception is captured as a newFailure(same behaviour asrecoverWith).
| Method | Returns | Mapper receives | Mapper returns |
|---|---|---|---|
flatMap | Try<B> | Value | Try<B> |
flatMapError | Try<Value> | Throwable | Try<Value> |
recoverWith | Try<Value> | Throwable | Try<Value> |
recover | Try<Value> | Throwable | Value |
flatMapErrorandrecoverWithare equivalent in most cases. PreferflatMapErrorwhen you think of the operation as “chaining on the error track”; preferrecoverWithwhen you think of it as “providing a fallback”.
Internal improvements
Bicontainer — shared interface
Common combinators (fold, getOrElse, getOrElseGet, getOrThrow, peek,
peekError, toOption, toResult) have been extracted from both Result and
Validated into the Bicontainer<Value, Error> shared interface. This eliminates
duplicate implementations and ensures both types honour exactly the same contracts.
sequence / traverse rewritten with Stream Gatherers
All sequence and traverse methods across Option, Result, and Try now use
Gatherer.ofSequential() instead of Collector.of() or manual iterator loops.
Stream<Result<V, E>> ──► Gatherer ──► Result<List<V>, E> stops at first Err (short-circuit)Iterable overloads delegate to their Stream counterparts via
StreamSupport.stream(iterable.spliterator(), false) — no more duplicated iteration logic.
Validated — record patterns and stream pipelines
Two internal implementations were modernised:
-
Validated.product(): the double-nestedswitchwas replaced with a localrecord Pair<X, Y>and a single exhaustive pattern-matchingswitch, reducing indentation and improving readability. -
Validated.traverse(Iterable): the anonymousIteratorinner class was replaced with aStreamSupport.stream(...).map(...).iterator()pipeline.
Getting the release
// Gradle (Kotlin DSL)implementation("codes.domix:fun:0.0.12")<!-- Maven --><dependency> <groupId>codes.domix</groupId> <artifactId>fun</artifactId> <version>0.0.12</version></dependency>Full Javadoc is available at /dmx-fun/javadoc/.
Found a bug or have a suggestion? Open an issue on GitHub.