Skip to content

Iso

An Iso<S, A> is a lossless, bidirectional conversion between two types. Both directions are total functions that are mutual inverses: converting S to A and back always yields the original value, and vice versa.

An Iso is the right optic when two types represent exactly the same information — a value-class wrapper around a primitive, a record paired with a named type, or a newtype alias.

Defining an Iso

Iso<S, A> takes a forward function (S -> A) and a reverse function (A -> S):

dart
// An Iso<S, A> is a lossless, bidirectional conversion between S and A.
// It requires two total functions that are mutual inverses:
//   get        : S -> A   (forward conversion)
//   reverseGet : A -> S   (backward conversion)

// Credentials can be treated as a plain (username, password) tuple — no
// information is gained or lost in either direction.
final credsIso = Iso<Credentials, (String, String)>(
  (c) => (c.username, c.password),
  (t) => t(Credentials.new),
);

Using an Iso

Because an Iso is also a Lens, it exposes the same get, replace, and modify operations:

dart
void isoUsage() {
  const creds = Credentials('admin', 's3cr3t');

  // get — convert S -> A
  final tuple = credsIso.get(creds); // ('admin', 's3cr3t')

  // reverseGet — convert A -> S
  final back = credsIso.reverseGet(('bob', 'hunter2')); // Credentials('bob','hunter2')

  // modify — apply a function in A-space, return a new S
  final renamed = credsIso.modify((t) => (t.$1.toUpperCase(), t.$2))(creds);
  // Credentials('ADMIN', 's3cr3t')
}

reverseGet constructs an S from an A — the direction a Lens cannot provide.

Reversing an Iso

reverse() flips the two types, returning a new Iso<A, S>:

dart
void reverseUsage() {
  // reverse() flips the direction: the result is an Iso<A, S>.
  final tupleToCredentials = credsIso.reverse();

  final creds = tupleToCredentials.get(('alice', 'pass')); // Credentials('alice','pass')
}

Composing Isos

andThen composes two Isos into a single Iso:

dart
// andThen composes two Isos into a single Iso.
final credsToLengths = credsIso.andThen(
  Iso<(String, String), (int, int)>(
    (t) => (t.$1.length, t.$2.length),
    (i) => (i.$1.toString(), i.$2.toString()),
  ),
);

void composeUsage() {
  const creds = Credentials('user', 'p');
  final lengths = credsToLengths.get(creds); // (4, 1)
}

Using an Iso as a Lens

Because Iso is a subtype of Lens, it can be passed anywhere a Lens is expected and composed with andThenL in a mixed optic chain:

dart
// An Iso is a Lens, so it can be composed with andThenL in a chain of optics
// that span product and sum types alike.
final dbCredsIso = Lens<AppConfig, DBConfig>(
      (cfg) => cfg.dbConfig,
      (db) => (cfg) => cfg.copy(dbConfig: db),
    )
    .andThenL(
      Lens<DBConfig, Credentials>(
        (db) => db.credentials,
        (c) => (db) => db.copy(credentials: c),
      ),
    )
    .andThenL(
      // An Iso satisfies the PLens interface, so andThenL accepts it directly.
      Lens<Credentials, String>(
        (c) => credsIso.get(c).$1,
        (u) => (c) => credsIso.modify((t) => (u, t.$2))(c),
      ),
    );

TIP

Reach for Iso when you have a wrapper type (a newtype, a value class) that only exists for type safety and carries no additional structure. The Iso makes the wrapped/unwrapped representations interchangeable without any runtime cost.