Skip to main content

Prism

A Prism<S, A> focuses on one variant of a sum type. The focus may or may not be present depending on which variant the value holds, so reads return Option<A>. Unlike an Optional, a Prism also knows how to construct an S from an A — wrapping a value back into the target variant.

A Prism is the right optic for:

  • Sealed class hierarchies where each subclass is a distinct case
  • Discriminated unions or tagged values
  • Extracting and constructing a specific variant without pattern-matching at every call site

Defining a Prism

Prism<S, A> takes two functions:

  • getOrModifyS -> Either<S, A>: Right(a) when the variant matches, Left(s) (the original value, unchanged) when it does not
  • reverseGetA -> S: wrap a back into the matching variant
Defining Prisms for a sealed hierarchy
// A sealed hierarchy representing a configuration value that can be
// a String, an int, or a boolean.
sealed class ConfigValue {}

final class CString extends ConfigValue {
final String value;
CString(this.value);
}

final class CInt extends ConfigValue {
final int value;
CInt(this.value);
}

final class CBool extends ConfigValue {
final bool value;
CBool(this.value);
}
Defining a Prism
// A Prism<S, A> focuses on one variant of a sum type.
// It requires two functions:
// getOrModify : S -> Either<S, A> (Right if the variant matches, Left otherwise)
// reverseGet : A -> S (construct the variant from A)

final stringP = Prism<ConfigValue, String>(
(cv) => switch (cv) {
CString(:final value) => Right(value),
_ => Left(cv),
},
CString.new,
);

final intP = Prism<ConfigValue, int>(
(cv) => switch (cv) {
CInt(:final value) => Right(value),
_ => Left(cv),
},
CInt.new,
);

final boolP = Prism<ConfigValue, bool>(
(cv) => switch (cv) {
CBool(:final value) => Right(value),
_ => Left(cv),
},
CBool.new,
);

Using a Prism

getOption / reverseGet / modify / replace
void prismUsage() {
final ConfigValue cv = CString('hello');

// getOption — Some when the variant matches, None otherwise
final str = stringP.getOption(cv); // Some('hello')
final num = intP.getOption(cv); // None

// reverseGet — always constructs the target variant
final constructed = intP.reverseGet(42); // CInt(42)

// modify — no-op when the variant does not match
final upper = stringP.modify((s) => s.toUpperCase())(cv); // CString('HELLO')
final unchanged = intP.modify((n) => n + 1)(cv); // CString('hello') — no match

// replace — convenience shorthand for modify((_) => value)
final replaced = stringP.replace('world')(cv); // CString('world')
}

modify and replace are no-ops when the variant does not match — the original value is returned unchanged.

Composing Prisms

andThenP composes two Prisms. The resulting Prism succeeds only when both match:

andThenP — Prism + Prism → Prism
// andThenP composes two Prisms: both variants must match for the result to succeed.
final positiveIntP = Prism<ConfigValue, int>(
(cv) => switch (cv) {
CInt(:final value) when value > 0 => Right(value),
_ => Left(cv),
},
CInt.new,
).andThenP(
Prism<int, String>(
(i) => i > 0 ? Right(i.toString()) : Left(i),
(s) => int.parse(s),
),
);

void composeUsage() {
final cv = CInt(7);
final str = positiveIntP.getOption(cv); // Some('7')

final neg = CInt(-1);
final noStr = positiveIntP.getOption(neg); // None
}
tip

Dart's exhaustive switch already handles sum-type matching at a single call site. Prism pays off when the same variant is extracted or constructed in multiple places, or when you want to pass the extraction logic as a value (e.g. to a higher-order combinator).