Limiting Traversals: Focusing on List Portions
Update the first or last few elements of a list, or a slice, and leave the rest alone.
- Focus part of a list by position with
taking,dropping,takingLast,droppingLastandslicing - Focus the leading run that meets a condition with
takingWhile, everything after it withdroppingWhile, or one index withelement - Predict what
modifyandgetAlldo outside the slice, and with a negative or oversized count - Compose a slice with lenses and filters, and apply two slices in sequence where
andThencannot chain them - Decide between a limiting traversal, a stream and a loop
In our journey through optics, we've seen how Traversal handles bulk operations on all elements of a collection (and the Precision and Filtering group's Filtered Optics will focus on elements matching a predicate). But what about focusing on elements by position: the first few items, the last few, or a specific slice?
Traditionally, working with list portions requires breaking out of your optic composition to use streams or manual index manipulation. Limiting traversals solve this elegantly by making positional focus a first-class part of your optic composition.
The Scenario: Product Catalogue Management
Imagine you're building an e-commerce platform where you need to:
- Display only the first 10 products on a landing page
- Apply discounts to all except the last 3 featured items
- Process customer orders in chunks of 50 for batch shipping
- Analyse the most recent 7 days of time-series sales data
- Update metadata for products between positions 5 and 15 in a ranked list
The Data Model:
@GenerateLenses
public record Product(String sku, String name, BigDecimal price, int stock) {
Product applyDiscount(int percent) {
BigDecimal factor = BigDecimal.valueOf(100 - percent, 2); // 10 percent off is 0.90
BigDecimal discounted = price.multiply(factor).setScale(2, RoundingMode.HALF_EVEN);
return new Product(sku, name, discounted, stock);
}
}
@GenerateLenses
public record Catalogue(String name, List<Product> products) {}
@GenerateLenses
public record Order(String id, List<LineItem> items, LocalDateTime created) {}
@GenerateLenses
public record LineItem(Product product, int quantity) {}
@GenerateLenses
public record SalesMetric(LocalDate date, BigDecimal revenue, int transactions) {}
The Traditional Approach:
// Verbose: Manual slicing breaks optic composition
List<Product> firstTen = catalogue.products().subList(0, Math.min(10, catalogue.products().size()));
List<Product> discounted = firstTen.stream()
.map(p -> p.applyDiscount(10))
.collect(Collectors.toList());
// Now reconstruct the full list... tedious!
List<Product> fullList = new ArrayList<>(discounted);
fullList.addAll(catalogue.products().subList(Math.min(10, catalogue.products().size()), catalogue.products().size()));
Catalogue updated = new Catalogue(catalogue.name(), fullList);
// Even worse with nested structures
List<Order> chunk = orders.subList(startIndex, Math.min(startIndex + chunkSize, orders.size()));
// Process chunk... then what? How do we put it back?
This approach forces you to abandon the declarative power of optics, manually managing indices, bounds checking, and list reconstruction. Limiting traversals let you express this intent directly within your optic composition.
A limiting traversal plays the part of a stream's limit(n) and skip(n). Unlike the stream, which drops the elements outside the range, a write through it keeps them, and returns a new list with only the chosen part changed.
Five Ways to Limit Focus by Index
Higher-Kinded-J's ListTraversals utility class provides five complementary index-based factory methods (two predicate-based companions and a single-element accessor follow later on this page):
| Method | Description | SQL Equivalent |
|---|---|---|
taking(n) | Focus on first n elements | LIMIT n |
dropping(n) | Skip first n, focus on rest | OFFSET n (then all) |
takingLast(n) | Focus on last n elements | ORDER BY id DESC LIMIT n |
droppingLast(n) | Focus on all except last n | LIMIT (size - n) |
slicing(from, to) | Focus on range [from, to) | LIMIT (to-from) OFFSET from |
Each serves different needs, and they can be combined with other optics for powerful compositions.
A Step-by-Step Walkthrough
Step 1: Basic Usage with taking(int n)
The most intuitive method: focus on at most the first n elements. The examples on this page use these imports:
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import org.higherkindedj.optics.Traversal;
import org.higherkindedj.optics.util.ListTraversals;
import org.higherkindedj.optics.util.Traversals;
taking(3) then focuses on the first three of five products:
// Create a traversal for first 3 products
Traversal<List<Product>, Product> first3 = ListTraversals.taking(3);
List<Product> products =
List.of(
new Product("SKU001", "Widget", new BigDecimal("10.00"), 100),
new Product("SKU002", "Gadget", new BigDecimal("25.00"), 50),
new Product("SKU003", "Gizmo", new BigDecimal("15.00"), 75),
new Product("SKU004", "Doohickey", new BigDecimal("30.00"), 25),
new Product("SKU005", "Thingamajig", new BigDecimal("20.00"), 60));
// Apply 10% discount to ONLY first 3 products
List<Product> result = Traversals.modify(first3, p -> p.applyDiscount(10), products);
// First 3 discounted; last 2 preserved unchanged
// Extract ONLY first 3 products
List<Product> firstThree = Traversals.getAll(first3, products);
// Widget, Gadget and Gizmo
Critical Semantic: During modification, non-focused elements are preserved unchanged in the structure. During queries (like getAll), they are excluded from the results. This preserves the overall structure whilst focusing operations on the subset you care about.
Step 2: Skipping Elements with dropping(int n)
Focus on all elements after skipping the first n:
// Skip first 2, focus on the rest
Traversal<List<Product>, Product> afterFirst2 = ListTraversals.dropping(2);
List<Product> result = Traversals.modify(afterFirst2, p -> p.applyDiscount(15), products);
// First 2 unchanged; last 3 get 15% discount
List<Product> skipped = Traversals.getAll(afterFirst2, products);
// Gizmo, Doohickey and Thingamajig
Step 3: Focusing on the End with takingLast(int n)
Focus on the last n elements, perfect for "most recent" scenarios:
// Focus on last 2 products
Traversal<List<Product>, Product> last2 = ListTraversals.takingLast(2);
List<Product> result = Traversals.modify(last2, p -> p.applyDiscount(20), products);
// First 3 unchanged; last 2 get 20% discount
List<Product> lastTwo = Traversals.getAll(last2, products);
// Doohickey and Thingamajig
Step 4: Excluding from the End with droppingLast(int n)
Focus on all elements except the last n:
// Focus on all except last 2
Traversal<List<Product>, Product> exceptLast2 = ListTraversals.droppingLast(2);
List<Product> result = Traversals.modify(exceptLast2, p -> p.applyDiscount(5), products);
// First 3 get 5% discount; last 2 unchanged
List<Product> allButLastTwo = Traversals.getAll(exceptLast2, products);
// Widget, Gadget and Gizmo
Step 5: Precise Slicing with slicing(int from, int to)
Focus on elements within a half-open range [from, to), exactly like List.subList():
// Focus on indices 1, 2, 3 (0-indexed, exclusive end)
Traversal<List<Product>, Product> slice = ListTraversals.slicing(1, 4);
List<Product> result = Traversals.modify(slice, p -> p.applyDiscount(12), products);
// Index 0 unchanged; indices 1-3 discounted; index 4 unchanged
List<Product> sliced = Traversals.getAll(slice, products);
// Gadget, Gizmo and Doohickey
Predicate-Based Focusing: Beyond Fixed Indices
Whilst index-based limiting is powerful, many real-world scenarios require conditional focusing: stopping when a condition is met rather than at a fixed position. ListTraversals provides two predicate-based methods, plus a single-element accessor, that complement the fixed-index approaches:
| Method | Description | Use Case |
|---|---|---|
takingWhile(Predicate) | Focus on longest prefix where predicate holds | Processing ordered data until threshold |
droppingWhile(Predicate) | Skip prefix whilst predicate holds | Ignoring header/preamble sections |
element(int) | Focus on single element at index (0-1 cardinality) | Safe indexed access without exceptions |
These methods enable runtime-determined focusing: the number of elements in focus depends on the data itself, not a predetermined count.
Step 6: Conditional Prefix with takingWhile(Predicate)
The takingWhile() method focuses on the longest prefix of elements satisfying a predicate. Once an element fails the test, traversal stops, even if later elements would pass.
// Focus on products whilst price < 20
Traversal<List<Product>, Product> affordablePrefix =
ListTraversals.takingWhile(p -> p.price().compareTo(new BigDecimal("20")) < 0);
List<Product> products =
List.of(
new Product("SKU001", "Widget", new BigDecimal("10.00"), 100),
new Product("SKU002", "Gadget", new BigDecimal("15.00"), 50),
new Product("SKU003", "Gizmo", new BigDecimal("25.00"), 75), // Stops here
new Product("SKU004", "Thing", new BigDecimal("12.00"), 25)); // Not included
// Apply discount only to initial affordable items
List<Product> result = Traversals.modify(affordablePrefix, p -> p.applyDiscount(10), products);
// Widget and Gadget discounted; Gizmo and Thing unchanged
// Extract the affordable prefix
List<Product> affordable = Traversals.getAll(affordablePrefix, products);
// Widget and Gadget: it stops at Gizmo, the first expensive item, so Thing is left out
Key Semantic: Unlike filtered(), which tests all elements, takingWhile() is sequential and prefix-oriented. It's the optics equivalent of Stream's takeWhile().
Real-World Use Cases:
- Time-series data: Process events before a timestamp threshold
- Sorted lists: Extract items below a value boundary
- Log processing: Capture startup messages before first error
- Priority queues: Handle high-priority items before switching logic
// Time-series: Process transactions before cutoff
LocalDateTime cutoff = LocalDateTime.of(2025, 1, 1, 0, 0);
Traversal<List<Transaction>, Transaction> beforeCutoff =
ListTraversals.takingWhile(t -> t.timestamp().isBefore(cutoff));
List<Transaction> processed = Traversals.modify(
beforeCutoff,
t -> t.withStatus("PROCESSED"),
transactions
);
Step 7: Skipping Prefix with droppingWhile(Predicate)
The droppingWhile() method is the complement to takingWhile(): it skips the prefix whilst the predicate holds, then focuses on all remaining elements.
// Skip low-stock products, focus on well-stocked ones
Traversal<List<Product>, Product> wellStocked =
ListTraversals.droppingWhile(p -> p.stock() < 50);
List<Product> products =
List.of(
new Product("SKU001", "Widget", new BigDecimal("10.00"), 20),
new Product("SKU002", "Gadget", new BigDecimal("25.00"), 30),
new Product("SKU003", "Gizmo", new BigDecimal("15.00"), 75), // First to pass
new Product("SKU004", "Thing", new BigDecimal("12.00"), 25)); // Included despite < 50
// Restock only well-stocked items (and everything after)
List<Product> restocked =
Traversals.modify(
wellStocked, p -> new Product(p.sku(), p.name(), p.price(), p.stock() + 50), products);
// Widget and Gadget unchanged; Gizmo and Thing restocked
List<Product> focused = Traversals.getAll(wellStocked, products);
// Gizmo and Thing
Real-World Use Cases:
- Skipping headers: Process CSV data after metadata rows
- Log analysis: Ignore initialisation messages, focus on runtime
- Pagination: Skip already-processed records in batch jobs
- Protocol parsing: Discard handshake, process payload
// Skip the leading configuration block in a log
Traversal<List<String>, String> runtimeLogs =
ListTraversals.droppingWhile(line -> line.startsWith("[CONFIG]"));
// Apply to log data
List<String> logs =
List.of(
"[CONFIG] Database URL",
"[CONFIG] Port",
"INFO: System started",
"ERROR: Connection failed");
List<String> result = Traversals.modify(runtimeLogs, String::toUpperCase, logs);
// [[CONFIG] Database URL, [CONFIG] Port, INFO: SYSTEM STARTED, ERROR: CONNECTION FAILED]
// Note: a [CONFIG] line appearing AFTER runtime lines would be modified too;
// droppingWhile only skips the leading prefix
Step 8: Single Element Access with element(int)
The element() method creates an affine traversal (0-1 cardinality) focusing on a single element at the given index. Unlike direct array access, it never throws IndexOutOfBoundsException.
// Focus on element at index 2
Traversal<List<Product>, Product> thirdProduct = ListTraversals.element(2);
List<Product> products =
List.of(
new Product("SKU001", "Widget", new BigDecimal("10.00"), 100),
new Product("SKU002", "Gadget", new BigDecimal("25.00"), 50),
new Product("SKU003", "Gizmo", new BigDecimal("15.00"), 75));
// Modify only the third product
List<Product> updated = Traversals.modify(thirdProduct, p -> p.applyDiscount(20), products);
// Only Gizmo discounted
// Extract the element (if present)
List<Product> element = Traversals.getAll(thirdProduct, products);
// Gizmo alone
// Out of bounds: gracefully returns empty
List<Product> outOfBounds = Traversals.getAll(ListTraversals.element(10), products);
// an empty list, and no exception
When to Use element() vs Ixed:
element(): For composition with other traversals, when index is known at construction timeIxed: For dynamic indexed access, more general type class approach
// Compose element() with nested structures (the explicit witness pins the element type)
Traversal<List<List<Product>>, Product> secondListThirdProduct =
ListTraversals.<List<Product>>element(1) // Second list
.andThen(ListTraversals.element(2)); // Third product in that list
// Ixed for dynamic access (see Indexed Access, in the Precision and Filtering group)
Optional<Product> chosen =
IxedInstances.get(IxedInstances.listIx(), userProvidedIndex, products);
Combining Predicate-Based and Index-Based Traversals
One thing andThen cannot do: chain two list-level slices. ListTraversals.taking(10).andThen(ListTraversals.takingWhile(...)) does not compile, because andThen continues from the element type, and a second slice needs the list. Apply slices in sequence instead:
// Take the first 10 products, then the leading in-stock run of those
List<Product> firstTen =
Traversals.getAll(ListTraversals.taking(10), products);
List<Product> steadyPrefix =
Traversals.getAll(ListTraversals.takingWhile(p -> p.stock() > 0), firstTen);
// A slice does compose with element-level optics: filter within the first ten
Traversal<List<Product>, Product> affordableOfFirstTen =
ListTraversals.<Product>taking(10).filtered(p -> p.price().compareTo(new BigDecimal("30")) < 0);
Edge Case Handling
All limiting traversal methods handle edge cases gracefully and consistently:
| Edge Case | Behaviour | Rationale |
|---|---|---|
n < 0 | Treated as 0: taking/takingLast focus nothing; dropping/droppingLast focus everything | Graceful degradation, no exceptions |
n > list.size() | Clamped to list bounds | Focus on all available elements |
| Empty list | Returns empty list unchanged | No elements to focus on |
from >= to in slicing | Empty traversal (no focus) | Empty range semantics |
Negative from in slicing | Clamped to 0 | Start from beginning |
// Examples of edge case handling
List<Integer> numbers = List.of(1, 2, 3);
// n > size: focuses on all elements
List<Integer> result1 = Traversals.getAll(ListTraversals.taking(100), numbers);
// [1, 2, 3]
// Negative n with taking: treated as 0, so no focus
List<Integer> result2 = Traversals.getAll(ListTraversals.taking(-5), numbers);
// []
// (dropping(-5) is also treated as dropping(0), which focuses on EVERY element)
// Inverted range: no focus
List<Integer> result3 = Traversals.getAll(ListTraversals.slicing(3, 1), numbers);
// []
// Empty list: safe operation
List<Integer> result4 = Traversals.modify(ListTraversals.taking(3), x -> x * 2, List.of());
// []
This philosophy ensures no runtime exceptions from index bounds, making limiting traversals safe for dynamic data.
Composing Limiting Traversals
The real power emerges when you compose limiting traversals with other optics:
With Lenses – Deep Updates
Traversal<List<Product>, Product> first5 = ListTraversals.taking(5);
Lens<Product, BigDecimal> priceLens = ProductLenses.price();
// Compose: first 5 products → their prices
Traversal<List<Product>, BigDecimal> first5Prices =
first5.andThen(priceLens);
// Increase prices of first 5 products by 10%, rounded back to pence
List<Product> result = Traversals.modify(
first5Prices,
price -> price.multiply(new BigDecimal("1.1")).setScale(2, RoundingMode.HALF_EVEN),
products);
With Filtered Traversals – Conditional Slicing
// First 10 products that are also low stock
Traversal<List<Product>, Product> first10LowStock =
ListTraversals.<Product>taking(10).filtered(p -> p.stock() < 50);
// Restock only first 10 low-stock products
List<Product> restocked = Traversals.modify(
first10LowStock,
p -> new Product(p.sku(), p.name(), p.price(), p.stock() + 100),
products
);
With Nested Structures – Batch Processing
// Focus on first 50 orders
Traversal<List<Order>, Order> first50Orders = ListTraversals.taking(50);
// Focus on all line items in those orders
Traversal<List<Order>, LineItem> first50OrderItems =
first50Orders.andThen(OrderTraversals.items());
// Apply bulk discount to items in first 50 orders
List<Order> processed = Traversals.modify(
first50OrderItems,
item -> new LineItem(item.product().applyDiscount(5), item.quantity()),
orders
);
When to Use Limiting Traversals vs Other Approaches
Use Limiting Traversals When
- Positional focus: You need to operate on elements by index position
- Structural preservation: Non-focused elements must remain in the list
- Composable pipelines: Building complex optic chains with lenses and prisms
- Immutable updates: Transforming portions whilst keeping data immutable
- Reusable logic: Define once, compose everywhere
// Perfect: Declarative, composable, reusable
Traversal<Catalogue, BigDecimal> first10Prices =
CatalogueLenses.products()
.andThen(ListTraversals.taking(10))
.andThen(ProductLenses.price());
Catalogue updated = Traversals.modify(
first10Prices, p -> p.multiply(new BigDecimal("0.9")).setScale(2, RoundingMode.HALF_EVEN), catalogue);
Use Stream API When
- Terminal operations: Counting, finding, collecting to new structures
- Complex transformations: Multiple chained operations with sorting/grouping
- No structural preservation needed: You're extracting data, not updating in place
- A hot loop you have measured: Production Readiness says what each call allocates
// Better with streams: Complex aggregation
int totalStock = products.stream()
.limit(100)
.mapToInt(Product::stock)
.sum();
Use Manual Loops When
- Early termination with side effects: Need to break out of loop
- Index-dependent logic: Processing depends on knowing the exact index
- Imperative control flow: Complex branching based on position
// Sometimes explicit indexing is clearest
for (int i = 0; i < Math.min(10, products.size()); i++) {
if (products.get(i).stock() == 0) {
notifyOutOfStock(products.get(i), i);
break;
}
}
Common Pitfalls
Don't Do This
// Inefficient: Recreating traversals in loops
for (int page = 0; page < totalPages; page++) {
var slice = ListTraversals.<Product>slicing(page * 10, (page + 1) * 10);
processPage(Traversals.getAll(slice, products));
}
// Confusing: Mixing with Stream operations unnecessarily
List<Product> result = Traversals.getAll(ListTraversals.<Product>taking(5), products)
.stream()
.limit(3) // Why limit again? Already took 5!
.collect(toList());
// Wrong expectation: Thinking it removes elements
Traversal<List<Product>, Product> first3 = ListTraversals.taking(3);
List<Product> modified = Traversals.modify(first3, p -> p.applyDiscount(10), products);
// modified.size() == products.size()! Structure preserved, not truncated
// Over-engineering: Using slicing for single element
Traversal<List<Product>, Product> atIndex5 = ListTraversals.slicing(5, 6);
// Consider using Ixed type class for single-element access instead
Do This Instead
// Efficient: Create traversal once, vary parameters
Traversal<List<Product>, Product> takeN(int n) {
return ListTraversals.taking(n);
}
// Or store commonly used ones as constants
static final Traversal<List<Product>, Product> FIRST_PAGE = ListTraversals.taking(10);
// Clear: Keep operations at appropriate abstraction level
List<Product> firstFive = Traversals.getAll(ListTraversals.taking(5), products);
// If you need further processing, do it separately
// Correct expectation: Use getAll for extraction, modify for transformation
List<Product> onlyFirst5 = Traversals.getAll(first5, products); // Extracts subset
List<Product> allWithFirst5Updated = Traversals.modify(first5, p -> p.applyDiscount(10), products); // Updates in place
// Right tool: Use Ixed for single indexed access
Optional<Product> fifth = IxedInstances.get(IxedInstances.listIx(), 4, products);
Real-World Example: E-Commerce Pagination
PaginationExample serves a catalogue of twenty products the way a REST endpoint would: a page at a time, with a hero section at the front and a clearance section at the back. Each product carries a stock count and a badge:
public record Product(
String sku, String name, BigDecimal price, int stock, boolean featured, String badge) {
Product withBadge(String newBadge) {
return new Product(sku, name, price, stock, featured, newBadge);
}
Product applyDiscount(int percent) {
BigDecimal factor = BigDecimal.valueOf(100 - percent, 2); // 30 percent off is 0.70
return new Product(
sku,
name,
price.multiply(factor).setScale(2, RoundingMode.HALF_EVEN),
stock,
featured,
badge);
}
Product markFeatured() {
return new Product(sku, name, price, stock, true, badge);
}
}
public record PageInfo(int pageNumber, int pageSize, int totalItems, int totalPages) {}
public record PagedResponse(List<Product> items, PageInfo pageInfo) {}
Each page is one slicing traversal, built from the page number:
private static PagedResponse getPage(List<Product> catalogue, int pageNumber, int pageSize) {
Traversal<List<Product>, Product> pageTraversal =
ListTraversals.slicing(pageNumber * pageSize, (pageNumber + 1) * pageSize);
List<Product> items = Traversals.getAll(pageTraversal, catalogue);
int totalPages = (int) Math.ceil(catalogue.size() / (double) pageSize);
PageInfo pageInfo = new PageInfo(pageNumber, pageSize, catalogue.size(), totalPages);
return new PagedResponse(items, pageInfo);
}
The hero section is the first three products. taking(3) marks them as featured, and composed with a price lens it discounts the same three, leaving every other price as it was:
private static void demonstrateFeaturedProducts(List<Product> catalogue) {
System.out.println("--- Scenario 2: Featured Products (Hero Section) ---");
// First 3 products are featured on the hero section
Traversal<List<Product>, Product> heroProducts = ListTraversals.taking(3);
// Mark them as featured and add "HOT" badge
List<Product> withHeroSection =
Traversals.modify(heroProducts, p -> p.markFeatured().withBadge("HOT"), catalogue);
System.out.println("Hero section products:");
Traversals.getAll(heroProducts, withHeroSection)
.forEach(
p ->
System.out.printf(
" ⭐ %s [%s] - Featured: %s%n", p.name(), p.badge(), p.featured()));
// Apply special 15% discount to hero products
Lens<Product, BigDecimal> priceLens =
Lens.of(
Product::price,
(prod, newPrice) ->
new Product(
prod.sku(),
prod.name(),
newPrice,
prod.stock(),
prod.featured(),
prod.badge()));
Traversal<List<Product>, BigDecimal> heroPrices = heroProducts.andThen(priceLens);
List<Product> discountedHero =
Traversals.modify(
heroPrices,
price -> price.multiply(new BigDecimal("0.85")).setScale(2, RoundingMode.HALF_EVEN),
catalogue);
System.out.println("\nAfter 15% hero discount:");
for (int i = 0; i < 5; i++) {
Product original = catalogue.get(i);
Product discounted = discountedHero.get(i);
String marker = i < 3 ? "★" : " ";
System.out.printf(
" %s %s: £%.2f → £%.2f%n",
marker, original.name(), original.price(), discounted.price());
}
System.out.println();
}
The clearance section is the last four. takingLast(4) discounts them, and droppingLast(4) counts the regular products that are left:
private static void demonstrateClearanceSection(List<Product> catalogue) {
System.out.println("--- Scenario 3: Clearance Section ---");
// Last 4 products are clearance items
Traversal<List<Product>, Product> clearanceItems = ListTraversals.takingLast(4);
System.out.println("Clearance items (last 4):");
List<Product> clearance = Traversals.getAll(clearanceItems, catalogue);
clearance.forEach(p -> System.out.printf(" 🏷️ %s - £%.2f%n", p.name(), p.price()));
// Apply 30% clearance discount
List<Product> withClearance =
Traversals.modify(
clearanceItems, p -> p.applyDiscount(30).withBadge("CLEARANCE"), catalogue);
System.out.println("\nAfter 30% clearance discount:");
Traversals.getAll(clearanceItems, withClearance)
.forEach(p -> System.out.printf(" 🏷️ %s [%s] - £%.2f%n", p.name(), p.badge(), p.price()));
// Regular items (all except clearance)
Traversal<List<Product>, Product> regularItems = ListTraversals.droppingLast(4);
List<Product> regular = Traversals.getAll(regularItems, catalogue);
System.out.println("\nRegular items (excluding clearance): " + regular.size() + " products");
System.out.println();
}
The hero and clearance sections print:
--- Scenario 2: Featured Products (Hero Section) ---
Hero section products:
⭐ Premium Electronics Item 1 [HOT] - Featured: true
⭐ Standard Home Item 2 [HOT] - Featured: true
⭐ Budget Garden Item 3 [HOT] - Featured: true
After 15% hero discount:
★ Premium Electronics Item 1: £17.50 → £14.88
★ Standard Home Item 2: £25.00 → £21.25
★ Budget Garden Item 3: £25.00 → £21.25
Deluxe Sports Item 4: £32.50 → £32.50
Basic Books Item 5: £40.00 → £40.00
--- Scenario 3: Clearance Section ---
Clearance items (last 4):
🏷️ Standard Home Item 17 - £100.00
🏷️ Budget Garden Item 18 - £100.00
🏷️ Deluxe Sports Item 19 - £107.50
🏷️ Basic Books Item 20 - £115.00
After 30% clearance discount:
🏷️ Standard Home Item 17 [CLEARANCE] - £70.00
🏷️ Budget Garden Item 18 [CLEARANCE] - £70.00
🏷️ Deluxe Sports Item 19 [CLEARANCE] - £75.25
🏷️ Basic Books Item 20 [CLEARANCE] - £80.50
Regular items (excluding clearance): 16 products
The Relationship to Functional Programming Libraries
For those familiar with functional programming, Higher-Kinded-J's limiting traversals are inspired by similar patterns in:
Haskell's Lens Library
The Control.Lens.Traversal module provides:
taking :: Int -> Traversal' [a] a
dropping :: Int -> Traversal' [a] a
These create traversals that focus on the first/remaining elements, exactly what our ListTraversals.taking() and dropping() do.
Scala's Monocle Library
Monocle provides similar index-based optics:
import monocle.function.Index._
// Focus on element at index
val atIndex: Optional[List[A], A] = index(3)
// Take first n (via custom combinator)
val firstN: Traversal[List[A], A] = ...
Key Differences in Higher-Kinded-J
- Explicit Applicative instances rather than implicit type class resolution
- Java's type system requires more explicit composition steps
- Additional methods like
takingLastanddroppingLastnot standard in Haskell lens - Edge case handling follows Java conventions (no exceptions, graceful clamping)
The limiting methods at a glance
| Method | Focus |
|---|---|
taking(n) | First n elements |
dropping(n) | Everything after the first n |
takingLast(n) | Last n elements |
droppingLast(n) | Everything except the last n |
slicing(from, to) | The index range [from, to) |
takingWhile(p) / droppingWhile(p) | The prefix a predicate accepts, or everything after it |
element(i) | The single element at index i |
- Positional focus stays inside the composition: "the first ten products" is an optic, chainable with lenses and filters, not a stream detour
- Out-of-range never throws: every limiting traversal degrades to fewer (or zero) targets instead of an
IndexOutOfBoundsException - Predicates complement indices:
takingWhile/droppingWhileslice by condition wheretaking/droppingslice by count - Structure is preserved: only the focused elements change; the list keeps its length and order
- This is Stream's
limit/skip, made composable: the same intent, expressed as a reusable, type-safe path
- List Decomposition: head/tail and init/last access as prisms and affines
- Filtered Optics: focusing by predicate across the whole list
- Traversals: the unrestricted bulk-update optic these methods refine
- Production Readiness: what a limiting traversal copies, and when to cache a composed optic
- Haskell: Lens Tutorial: original inspiration for optics
- Chris Penner: Optics By Example: comprehensive book on optics (Haskell)
- Scala: Monocle Documentation: optics library with similar patterns
- Java: Stream API: comparison with
limit()andskip()
Previous: Common Data Structures Next: List Decomposition