The Const Type: Constant Functors with Phantom Types 📌
- Understanding phantom types and how Const ignores its second type parameter
- Using Const for efficient fold implementations and data extraction
- Leveraging Const with bifunctor operations to transform constant values
- Applying Const in lens and traversal patterns for compositional getters
- Real-world use cases in validation, accumulation, and data mining
- How Const relates to Scala's Const and van Laarhoven lenses
The Const type is a constant functor that holds a value of type C whilst treating A as a phantom type parameter—a type that exists only in the type signature but has no runtime representation. This seemingly simple property unlocks powerful patterns for accumulating values, implementing efficient folds, and building compositional getters in the style of van Laarhoven lenses.
New to phantom types? See the Glossary for a detailed explanation with Java-focused examples, or continue reading for practical demonstrations.
What is Const?
A Const<C, A> is a container that holds a single value of type C. The type parameter A is phantom—it influences the type signature for composition and type safety but doesn't correspond to any stored data. This asymmetry is the key to Const's utility.
// Create a Const holding a String, with Integer as the phantom type
Const<String, Integer> stringConst = new Const<>("Hello");
// The constant value is always accessible
String value = stringConst.value(); // "Hello"
// Create a Const holding a count, with Person as the phantom type
Const<Integer, Person> countConst = new Const<>(42);
int count = countConst.value(); // 42
Key Characteristics
- Constant value: Holds a value of type
Cthat can be retrieved viavalue() - Phantom type: The type parameter
Aexists only for type-level composition - Bifunctor instance: Implements
Bifunctor<ConstKind2.Witness>where:first(f, const)transforms the constant valuesecond(g, const)changes only the phantom type, leaving the constant value unchangedbimap(f, g, const)combines both transformations (but onlyfaffects the constant)
Core Components
The Const Type
public record Const<C, A>(C value) {
public <D> Const<D, A> mapFirst(Function<? super C, ? extends D> firstMapper);
public <B> Const<C, B> mapSecond(Function<? super A, ? extends B> secondMapper);
public <D, B> Const<D, B> bimap(
Function<? super C, ? extends D> firstMapper,
Function<? super A, ? extends B> secondMapper);
}
The HKT Bridge for Const
ConstKind2<C, A>: The HKT marker interface extendingKind2<ConstKind2.Witness, C, A>ConstKind2.Witness: The phantom type witness for Const in the Kind2 systemConstKindHelper: Utility providingwiden2andnarrow2for Kind2 conversions
Type Classes for Const
ConstBifunctor: The singleton bifunctor instance implementingBifunctor<ConstKind2.Witness>
The Phantom Type Property
The defining characteristic of Const is that mapping over the second type parameter has no effect on the constant value. This property is enforced both conceptually and at runtime.
import static org.higherkindedj.hkt.constant.ConstKindHelper.CONST;
Bifunctor<ConstKind2.Witness> bifunctor = ConstBifunctor.INSTANCE;
// Start with a Const holding an integer count
Const<Integer, String> original = new Const<>(42);
System.out.println("Original value: " + original.value());
// Output: 42
// Use second() to change the phantom type from String to Double
Kind2<ConstKind2.Witness, Integer, Double> transformed =
bifunctor.second(
s -> s.length() * 2.0, // Function defines phantom type transformation
CONST.widen2(original));
Const<Integer, Double> result = CONST.narrow2(transformed);
System.out.println("After second(): " + result.value());
// Output: 42 (UNCHANGED!)
// The phantom type changed (String -> Double), but the constant value stayed 42
Note: Whilst the mapper function in second() is never applied to actual data (since A is phantom), it is still validated and applied to null for exception propagation. This maintains consistency with bifunctor exception semantics.
Const as a Bifunctor
Const naturally implements the Bifunctor type class, providing three fundamental operations:
1. first() - Transform the Constant Value
The first operation transforms the constant value from type C to type D, leaving the phantom type unchanged.
Const<String, Integer> stringConst = new Const<>("hello");
// Transform the constant value from String to Integer
Kind2<ConstKind2.Witness, Integer, Integer> lengthConst =
bifunctor.first(String::length, CONST.widen2(stringConst));
Const<Integer, Integer> result = CONST.narrow2(lengthConst);
System.out.println(result.value()); // Output: 5
2. second() - Transform Only the Phantom Type
The second operation changes the phantom type from A to B without affecting the constant value.
Const<String, Integer> stringConst = new Const<>("constant");
// Change the phantom type from Integer to Boolean
Kind2<ConstKind2.Witness, String, Boolean> boolConst =
bifunctor.second(i -> i > 10, CONST.widen2(stringConst));
Const<String, Boolean> result = CONST.narrow2(boolConst);
System.out.println(result.value()); // Output: "constant" (unchanged)
3. bimap() - Transform Both Simultaneously
The bimap operation combines both transformations, but remember: only the first function affects the constant value.
Const<String, Integer> original = new Const<>("hello");
Kind2<ConstKind2.Witness, Integer, String> transformed =
bifunctor.bimap(
String::length, // Transforms constant: "hello" -> 5
i -> "Number: " + i, // Phantom type transformation only
CONST.widen2(original));
Const<Integer, String> result = CONST.narrow2(transformed);
System.out.println(result.value()); // Output: 5
Use Case 1: Efficient Fold Implementations
One of the most practical applications of Const is implementing folds that accumulate a single value whilst traversing a data structure. The phantom type represents the "shape" being traversed, whilst the constant value accumulates the result.
// Count elements in a list using Const
List<String> items = List.of("apple", "banana", "cherry", "date");
Const<Integer, String> count = items.stream()
.reduce(
new Const<>(0), // Initial count
(acc, item) -> new Const<Integer, String>(acc.value() + 1), // Increment
(c1, c2) -> new Const<>(c1.value() + c2.value())); // Combine
System.out.println("Count: " + count.value());
// Output: 4
// Accumulate total length of all strings
Const<Integer, String> totalLength = items.stream()
.reduce(
new Const<>(0),
(acc, item) -> new Const<Integer, String>(acc.value() + item.length()),
(c1, c2) -> new Const<>(c1.value() + c2.value()));
System.out.println("Total length: " + totalLength.value());
// Output: 23
In this pattern, the phantom type (String) represents the type of elements we're folding over, whilst the constant value (Integer) accumulates the result. This mirrors the implementation of folds in libraries like Cats and Scalaz in Scala.
Use Case 2: Getters and Van Laarhoven Lenses
Const is fundamental to the lens pattern pioneered by Edward Kmett and popularised in Scala libraries like Monocle. A lens is an abstraction for focusing on a part of a data structure, and Const enables the "getter" half of this abstraction.
The Getter Pattern
A getter extracts a field from a structure without transforming it. Using Const, we represent this as a function that produces a Const where the phantom type tracks the source structure.
record Person(String name, int age, String city) {}
record Company(String name, Person ceo) {}
Person alice = new Person("Alice", 30, "London");
Company acmeCorp = new Company("ACME Corp", alice);
// Define a getter using Const
Function<Person, Const<String, Person>> nameGetter =
person -> new Const<>(person.name());
// Extract the name
Const<String, Person> nameConst = nameGetter.apply(alice);
System.out.println("CEO name: " + nameConst.value());
// Output: Alice
// Define a getter for the CEO from a Company
Function<Company, Const<Person, Company>> ceoGetter =
company -> new Const<>(company.ceo());
// Compose getters: get CEO name from Company using mapFirst
Function<Company, Const<String, Company>> ceoNameGetter = company ->
ceoGetter.apply(company)
.mapFirst(person -> nameGetter.apply(person).value());
Const<String, Company> result = ceoNameGetter.apply(acmeCorp);
System.out.println("Company CEO name: " + result.value());
// Output: Alice
This pattern is the foundation of van Laarhoven lenses, where Const is used with Functor or Applicative to implement compositional getters. For a deeper dive, see Van Laarhoven Lenses and Scala Monocle.
Use Case 3: Data Extraction from Validation Results
When traversing validation results, you often want to extract accumulated errors or valid data without transforming the individual results. Const provides a clean way to express this pattern.
record ValidationResult(boolean isValid, List<String> errors, Object data) {}
List<ValidationResult> results = List.of(
new ValidationResult(true, List.of(), "Valid data 1"),
new ValidationResult(false, List.of("Error A", "Error B"), null),
new ValidationResult(true, List.of(), "Valid data 2"),
new ValidationResult(false, List.of("Error C"), null)
);
// Extract all errors using Const
List<String> allErrors = new ArrayList<>();
for (ValidationResult result : results) {
// Use Const to extract errors, phantom type represents ValidationResult
Const<List<String>, ValidationResult> errorConst = new Const<>(result.errors());
allErrors.addAll(errorConst.value());
}
System.out.println("All errors: " + allErrors);
// Output: [Error A, Error B, Error C]
// Count valid results
Const<Integer, ValidationResult> validCount = results.stream()
.reduce(
new Const<>(0),
(acc, result) -> new Const<Integer, ValidationResult>(
result.isValid() ? acc.value() + 1 : acc.value()),
(c1, c2) -> new Const<>(c1.value() + c2.value()));
System.out.println("Valid results: " + validCount.value());
// Output: 2
The phantom type maintains the "context" of what we're extracting from (ValidationResult), whilst the constant value accumulates the data we care about (errors or counts).
Const vs Other Types
Understanding how Const relates to similar types clarifies its unique role:
| Type | First Parameter | Second Parameter | Primary Use |
|---|---|---|---|
Const<C, A> | Constant value (stored) | Phantom (not stored) | Folds, getters, extraction |
Tuple2<A, B> | First element (stored) | Second element (stored) | Pairing related values |
Identity<A> | Value (stored) | N/A (single parameter) | Pure computation wrapper |
Either<L, R> | Error (sum type) | Success (sum type) | Error handling |
Use Const when:
- You need to accumulate a single value during traversal
- You're implementing getters or read-only lenses
- You want to extract data without transformation
- The phantom type provides useful type-level information for composition
Use Tuple2 when:
- You need to store and work with both values
- Both parameters represent actual data
Use Identity when:
- You need a minimal monad wrapper with no additional effects
Exception Propagation Note
Although mapSecond doesn't transform the constant value, the mapper function is still applied to null to ensure exception propagation. This maintains consistency with bifunctor semantics.
Const<String, Integer> const_ = new Const<>("value");
// This will throw NullPointerException from the mapper
Const<String, Double> result = const_.mapSecond(i -> {
if (i == null) throw new NullPointerException("Expected non-null");
return i * 2.0;
});
This behaviour ensures that invalid mappers are detected, even though the mapper's result isn't used. For null-safe mappers, simply avoid dereferencing the parameter:
// Null-safe phantom type transformation
Const<String, Double> safe = const_.mapSecond(i -> 3.14);
Summary
- Const<C, A> holds a constant value of type
Cwith a phantom type parameterA - Phantom types exist only in type signatures, enabling type-safe composition without runtime overhead
- Bifunctor operations:
firsttransforms the constant valuesecondchanges only the phantom typebimapcombines both (but only affects the constant via the first function)
- Use cases:
- Efficient fold implementations that accumulate a single value
- Compositional getters in lens and traversal patterns
- Data extraction from complex structures without transformation
- Scala heritage: Mirrors
Constin Cats, Scalaz, and Monocle - External resources:
Understanding Const empowers you to write efficient, compositional code for data extraction and accumulation, leveraging patterns battle-tested in the Scala functional programming ecosystem.