Lens
A Lens<S, A> focuses on a field that is always present inside S. It is the right optic whenever the target field is a required (non-optional) part of the structure — which covers the vast majority of product-type fields.
A Lens provides two primitive operations:
get(s)— read the focused valuereplace(a)(s)— return a newSwith the focused value replacedmodify(f)(s)— return a newSwith the focused value transformed byf
Defining a Lens
Lens<S, A> takes two functions: a getter and a setter. The setter follows the curried form (A) => (S) => S so it composes cleanly without needing a two-argument lambda:
// A Lens<S, A> requires two functions:
// get : S -> A (read the focused field)
// set : (A, S) -> S (replace the focused field, returning a new S)
final dbConfigL = Lens<AppConfig, DBConfig>(
(cfg) => cfg.dbConfig,
(db) => (cfg) => cfg.copy(dbConfig: db),
);
final portL = Lens<DBConfig, int>(
(db) => db.port,
(p) => (db) => db.copy(port: p),
);Using a Lens
void lensUsage() {
// get — read the focused value
final db = dbConfigL.get(sampleConfig); // DBConfig(...)
final port = portL.get(sampleConfig.dbConfig); // 5432
// replace — return a new AppConfig with dbConfig replaced
final newConfig = dbConfigL.replace(
const DBConfig(Credentials('admin', 's3cr3t'), 6543, None()),
)(sampleConfig);
// modify — apply a function to the focused value
final bumped = portL.modify((p) => p + 1)(sampleConfig.dbConfig);
}replace and modify both return a Function1<S, S> — a function from the old structure to the new one. This is the standard optic application style: lens.replace(newValue)(myStruct).
Composing Lenses
Lenses compose with andThenL. The result is a new Lens that focuses through both layers in sequence:
// Lenses compose with andThenL, producing a new Lens that focuses deeper.
final dbPortL = dbConfigL.andThenL(portL);
void composeUsage() {
final port = dbPortL.get(sampleConfig); // 5432
// Update the port two levels deep in one step.
final updated = dbPortL.replace(9999)(sampleConfig);
}The composition is transparent — callers interact with dbPortL exactly like any other Lens<AppConfig, int>.
Composing with a Getter
andThenG pairs a Lens with a read-only Getter to project a derived value. The result is a Getter, so it supports get but not replace or modify:
// andThenG composes a Lens with a read-only Getter.
// Useful when the target type has derived values you want to project.
final versionMajorG = Lens<AppConfig, String>(
(cfg) => cfg.version,
(v) => (cfg) => cfg.copy(version: v),
).andThenG(Getter((String v) => int.parse(v.split('.').first)));
void getterUsage() {
final major = versionMajorG.get(sampleConfig); // 2
}TIP
Use andThenG when the target type has a meaningful derived value (a count, a formatted string, a sub-field) that you never need to write back. It keeps the optic read-only by type, preventing accidental mutations.