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:
getOrModify—S -> Either<S, A>:Right(a)when the variant matches,Left(s)(the original value, unchanged) when it does notreverseGet—A -> S: wrapaback into the matching variant
// 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);
}
// 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
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 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
}
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).