Optional
An Optional<S, A> focuses on a value that may or may not be present inside S. Reads return Option<A>, and writes are applied only when the value is present — when it is absent the structure is returned unchanged.
Optional is the most general read-write optic. Every Lens is an Optional (a field that is always present is trivially "present"), and every Prism is an Optional (a matching variant is present; a non-matching one is absent). It is the right direct choice when the focus is an Option<A> field or any other field whose existence depends on runtime state.
Defining an Optional
Optional<S, A> takes two functions:
getOrModify—S -> Either<S, A>:Right(a)when the value is present,Left(s)(unchanged) when it is absentset—(A) => (S) => S: replace the focused value
// An Optional<S, A> focuses on a value that may or may not be present.
// It generalises both Lens (always present) and Prism (variant matching).
//
// It requires two functions:
// getOrModify : S -> Either<S, A> (Right when the value is present)
// set : (A, S) -> S (replace — only applied when present)
// Focus on the optional host field inside DBConfig.
final hostO = Optional<DBConfig, String>(
(db) => db.host.toRight(() => db),
(h) => (db) => db.copy(host: h.some),
);Using an Optional
void optionalUsage() {
final withHost = DBConfig(
const Credentials('admin', 's3cr3t'),
5432,
'db.local'.some,
);
final noHost = DBConfig(const Credentials('admin', 's3cr3t'), 5432, none());
// getOption — Some when present, None when absent
final h1 = hostO.getOption(withHost); // Some('db.local')
final h2 = hostO.getOption(noHost); // None
// replace — updates only when the value is present; no-op otherwise
final updated = hostO.replace('prod.db')(withHost); // host = 'prod.db'
final unchanged = hostO.replace('prod.db')(noHost); // host still absent
// modify — same conditional semantics
final upper = hostO.modify((h) => h.toUpperCase())(withHost);
// modifyOption — Some(updated S) when present, None when absent
final opt = hostO.modifyOption((h) => h.toUpperCase())(withHost);
final none_ = hostO.modifyOption((h) => h.toUpperCase())(noHost);
}The conditional behaviour of replace and modify is what distinguishes Optional from Lens: calling them on a structure where the focus is absent returns the original structure untouched.
modifyOption makes the conditionality explicit: Some(updatedS) when the value was present and the modification applied, None when it was absent.
Composing Optionals
andThenO composes a Lens (or another Optional) with an Optional. The result is an Optional that digs through both layers:
// Compose a Lens with an Optional using andThenO.
// The result is an Optional that digs through both layers.
final dbConfigL = Lens<AppConfig, DBConfig>(
(cfg) => cfg.dbConfig,
(db) => (cfg) => cfg.copy(dbConfig: db),
);
final appHostO = dbConfigL.andThenO(hostO);
void composeUsage() {
// Reads the host two levels deep; None when absent at either level.
final host = appHostO.getOption(sampleConfig); // Some('db.local')
// Updates the host two levels deep in one step.
final updated = appHostO.replace('new.host')(sampleConfig);
}Because a Lens is a subtype of Optional, andThenO accepts a Lens on the left-hand side. This means you can combine required and optional layers freely in a single optic chain.