Vavr Functional
Mi ez és mikor kell
- A JDK-ból hiányzó funkcionális építőkövek:
Option, Try, Either, Validation, persistent collections, Tuple, Lazy. - Mind a
Value leszármazottja, ezért közös API: getOrElse, isEmpty, toList, toOption, toJavaOptional. - Ma a JDK sokat kivált: sealed + record +
switch pattern matching = Either/Match library nélkül. - Vavr marad értelmes:
Validation, Try, persistent collections.
Option
Option.of(null); // None
Option.some(null); // Some(null) <- csapda
Option.of(5).map(x -> (Integer) null); // Some(null)! Optional.map itt empty lenne
Option.of(5).flatMap(x -> Option.of(foo(x))); // ez a null-biztos változat
- Serializable, Iterable, gazdagabb API, mint az
Optional.
Try
Try.of(() -> client.fetch(id)) // checked exception is mehet
.recover(IOException.class, e -> cached(id))
.onFailure(e -> log.warn("fetch failed", e))
.getOrElse(DEFAULT);
// Success(value) | Failure(ex)
- Csak non-fatal exceptiont fog el.
InterruptedException, VirtualMachineError, LinkageError, ThreadDeath továbbmegy. - Mire jó: exceptiont dobó külső hívás becsomagolására. Tipizált domain hibára az
Either jobb. Try.withResources a try-with-resources megfelelője.
Either (right-biased)
Either<LedgerError, Receipt> r =
findAccount(id) // Either<LedgerError, Account>
.flatMap(a -> debit(a, amt)) // Left esetén kimarad
.map(Receipt::of);
r.fold(this::toProblemDetail, Receipt::toResponse); // boundary
- Left = hiba, Right = siker.
flatMap fail-fast: az első Left megszakítja a láncot. - Mikor: várt üzleti hiba (insufficient funds). Exception a váratlan technikai hibára.
- Hasznos metódusok:
map / mapLeft / bimap(fl, fr) filterOrElse(pred, r -> err): Right-ból Left, ha a feltétel nem teljesül orElse(() -> alt): fallback swap(): oldalak felcserélése peek / peekLeft: side effect (logolás) getOrElseThrow(l -> new XException(l)): vissza a kivételvilágba
Validation (error accumulation)
Validation<Seq<String>, Transfer> v = Validation.combine(
validAmount(amt), validAccount(from), validAccount(to)
).ap(Transfer::new);
// Invalid(List("amount <= 0", "unknown account")) <- minden hiba együtt
- Either = fail-fast, Validation = accumulate (applicative).
- Függő lépések →
Either + flatMap. Független ellenőrzések → Validation.combine.
Átjárhatóság (konverziók)
| Honnan → hova | Hogyan |
Optional → Option | Option.ofOptional(opt) |
Option → Optional | .toJavaOptional() |
Option → Either | .toEither(() -> err) (a None-hoz kell hiba) |
Option → Try | .toTry() |
Try → Either | .toEither() (Left = Throwable), utána mapLeft |
Either → Option | .toOption() (a Left eltűnik) |
Either ↔ Validation | Validation.fromEither(e) / validation.toEither() |
Vegyes lánc (példa)
Either<LedgerError, Receipt> transfer(String id, BigDecimal amt) {
return Option.ofOptional(repo.findById(id)) // Optional -> Option
.<LedgerError>toEither(() -> new NotFound(id)) // Option -> Either
.flatMap(acc -> Try.of(() -> gateway.debit(acc, amt))
.toEither() // Try -> Either<Throwable, ...>
.<LedgerError>mapLeft(GatewayError::new)) // hibatípus egységesítése
.map(Receipt::of);
}
// nincs account -> Left(NotFound)
// gateway hiba -> Left(GatewayError)
// siker -> Right(Receipt)
Gotchák
Either<L,R> invariáns L-ben: a flatMap azonos L-t vár. Megoldás: sealed LedgerError hierarchia + mapLeft vagy type witness (.<LedgerError>toEither(...)). @Transactional csak kivételre rollbackel, Left-re nem. Vagy dobsz a boundary-nél, vagy setRollbackOnly(). Ledger-doménben klasszikus bug. Either → Option információvesztés. - Ne keverd a hibakezelési stílusokat egy rétegben. Belül
Either, a határon fold vagy throw.
Sequence (több elem együtt)
Option.sequence, Try.sequence: Iterable<Option<T>> → Option<Seq<T>>. Egy None vagy Failure elbuktatja az egészet. Either.sequenceRight: fail-fast, az első Left-tel tér vissza. Either.sequence: az összes Left-et gyűjti. traverse ugyanez mappeléssel. Pontos szignatúra: API doc.
Persistent collections
List<Integer> a = List.of(1, 2, 3); // io.vavr.collection.List
List<Integer> b = a.prepend(0); // O(1), a változatlan, b megosztja a farkát
java.util.List<Integer> j = b.toJavaList();
- Immutable + structural sharing: módosítás = új példány, a régi rész megosztva marad.
List (linked), Vector, HashMap/HashSet, TreeMap, lazy Stream. - Nem
java.util.Collection, konverzió: toJavaList(), toJavaMap() stb. - JDK-val összevetve:
List.of (JDK): immutable, de módosításhoz teljes másolat kell Collections.unmodifiableList: csak view, az eredeti módosítható marad - Vavr: hatékony immutable módosítás
Egyéb
Tuple1..8, Lazy (memoized supplier), Function2.memoized(), curried(), andThen. Match(x).of(Case($(...), ...)): elavult, a JDK switch pattern matching váltja.
JDK alternatíva: Result sealed + record
sealed interface Result<T> permits Ok, Err {}
record Ok<T>(T value) implements Result<T> {}
record Err<T>(LedgerError error) implements Result<T> {}
String msg = switch (result) {
case Ok<Receipt>(var v) -> "OK " + v.id();
case Err<Receipt>(var e) -> "ERR " + e;
};
- Előny: nincs dependency, exhaustiveness check a fordítótól.
- Hátrány: nincs
map/flatMap/sequence, ezeket magadnak kell megírni.
Hol találkozol Vavr-ral
- Resilience4j (belül használja).
- Spring Data: Vavr collection return type támogatás.
vavr-jackson: Jackson modul Vavr típusokhoz.