Control types¶
The control types hold the result of a step: a value that may be absent, a success or a failure, or a value computed later. Each one has its own page:
Option: a value that may be absent.Either: a result or an error, stopping at the first error.Try: a computation that may have thrown.Using: resources released after use, with the outcome in aTry.Validation: checks that keep every error.Lazy: a value computed on first access, then cached.
Values, not collections¶
Option, Either, Try and Validation are sealed interfaces whose cases are records. Pattern matching on them with
a switch needs no default, and the compiler checks that every case is handled.
None of them is Iterable. Each has a short list of explicit conversions instead.
Lazy is a class of its own: it has no cases, is never empty, and has no failure side.
Which one to pick¶
| You have | Use | Cases |
|---|---|---|
| a value that may be missing, and nothing to say about why | Option<A> |
Some(A value), None() |
| a result or an error value, and the first error stops the work | Either<L, R> |
Left(L value), Right(R value) |
| code that throws exceptions | Try<A> |
Success(A value), Failure(Throwable cause) |
| independent checks, and you want every error at once | Validation<E, A> |
Valid(A value), Invalid(NonEmptyVector<E> errors) |
| a value that is costly to compute and may not be needed | Lazy<A> |
none: get() computes it |
Members they share¶
The types share most of their members, with the same names and the same argument order, failure side first:
| Member | Option |
Either |
Try |
Validation |
|---|---|---|---|---|
map, flatMap |
on Some |
on Right |
on Success |
on Valid |
fold(ifFailure, ifSuccess) |
Supplier, Function |
Function<L>, Function<R> |
Function<Throwable>, Function<A> |
Function<NonEmptyVector<E>>, Function<A> |
getOrElse(value), getOrElse(Supplier) |
yes | yes, plus getOrElse(Function<L, R>) |
yes, plus getOrElse(Function<Throwable, A>) |
yes, plus getOrElse(Function<NonEmptyVector<E>, A>) |
getOrElseThrow, getOrNull |
yes | yes | yes | yes |
contains, exists, forAll, forEach |
yes | on the right side | on the success | on the valid value |
tap |
tap, tapNone |
tap, tapLeft |
tap, tapError |
tap, tapError |
orElse |
orElse(other), orElse(Supplier) |
orElse(other), orElse(Supplier) |
orElse(other), orElse(Supplier) |
orElse(Supplier) |
zip, zipWith, zipLeft, zipRight |
yes | yes | yes | yes, keeping every error |
static collectAll, forEach, flatten |
yes | yes | yes | yes, keeping every error |
Lazy has map, flatMap, tap, the zip family, and the static collectAll and flatten.
Many values at once¶
The static collectAll turns a collection of values into one value holding a Vector. The static forEach does the
same after applying a function to each element.
Option, Either and Try stop at the first None, Left or Failure. Validation keeps going and gathers
every error.
var all = Option.collectAll(Vector.of(Option.some(1), Option.some(2))); // Option<Vector<Integer>>
var parsed = Either.forEach(Vector.of("1", "x", "3"), // Either<String, Vector<Integer>>
s -> s.chars().allMatch(Character::isDigit) ? Either.right(Integer.parseInt(s)) : Either.left("bad: " + s));
// Some(Vector(1, 2)), Left(bad: x)
Nested values¶
The static flatten removes one level of nesting, on every type including Validation and Lazy.
var flat = Option.flatten(Option.some(Option.some(1))); // Option<Integer>
var inner = Either.flatten(Either.right(Either.<String, Integer>left("inner failure")));
// Some(1), Left(inner failure)
Conversions¶
The conversions are explicit and short; there is no Iterable to lean on.
| From | Conversions |
|---|---|
Option |
toEither(Supplier<L>), toTry(Supplier<Throwable>), toValidation(Supplier<E>), toVector(), toList(), toOptional(), stream(); Option.ofOptional(Optional) the other way |
Either |
toOption(), toTry(Function<L, Throwable>), toValidation(), toVector() |
Try |
toOption(), toEither(), toValidation(), toVector(), toCompletableFuture(); Try.fromCompletableFuture the other way |
Validation |
toOption(), toEither(), toEitherWith(Function), toTry(Function), toVector(); fromOption, fromEither, fromTry the other way |
Lazy |
get(), toSupplier() |
var fromOption = Option.<Integer>none().toEither(() -> "missing"); // Either<String, Integer>
var optional = Option.some(5).toOptional(); // java.util.Optional<Integer>
var fromTry = Try.of(() -> Integer.parseInt("7")).toOption(); // Option<Integer>
// Left(missing), Optional[5], Some(7)
Null policy¶
Some, Left, Right, Success and Valid never hold null:
- Creating one with
nullthrows aNullPointerException. Option.ofNullableturns a value that may benullinto anOption, andgetOrNull()goes back.- A
Trywhose computation returnsnullis aFailureholding aNullPointerException. - A function you pass that returns
nullwhere a value is needed, such as the mapper ofmaporflatMapor the supplier oforElse, throws aNullPointerExceptionnaming the method:Option.flatMap: mapper returned null. WhenTryruns the function, as inTry.maporTry.flatMap, you get aFailureof it instead.
var absent = Option.<String>ofNullable(null); // Option<String>
var nullResult = Try.<String>of(() -> null); // Try<String>
var npe = nullResult.getCause() instanceof NullPointerException;
// None, Failure(java.lang.NullPointerException: ...), true
Lazy is the exception: it may hold null. Wrap its value explicitly, with Option.ofNullable(lazy.get()).