Skip to content

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:

  • getOrModifyS -> Either<S, A>: Right(a) when the value is present, Left(s) (unchanged) when it is absent
  • set(A) => (S) => S: replace the focused value
dart
// 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

dart
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:

dart
// 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.

TIP

Reach for Optional over Lens whenever the field type is Option<A> or the focus depends on runtime state. Wrap the absent case in Left(s) in getOrModify to leave the structure unchanged when the value is missing.