Skip to content

Commit 99d09dd

Browse files
committed
Add docs
1 parent 9ff7122 commit 99d09dd

1 file changed

Lines changed: 140 additions & 7 deletions

File tree

docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -702,13 +702,6 @@ You can also provide your own collectors by implementing the
702702
`ai.timefold.solver.core.api.score.stream.uni.UniConstraintCollector` interface,
703703
or its `Bi...`, `Tri...` and `Quad...` counterparts.
704704

705-
NOTE: Custom collectors can opt into incremental mode by having their `accumulator()` method return a
706-
`*ConstraintCollectorAccumulator` instance (e.g. `UniConstraintCollectorAccumulator`) instead of a plain lambda.
707-
In incremental mode, the accumulator receives insert/update/retract calls individually,
708-
which is more efficient than the default full retract-and-reinsert on update.
709-
The solver detects incremental support via `instanceof` automatically — no other method needs overriding.
710-
`*ConstraintCollectorAccumulator` extends the corresponding function type but throws `UnsupportedOperationException`
711-
if called as a function; use it via its `intoGroup()` method instead.
712705

713706

714707
[#collectorsCount]
@@ -1194,6 +1187,146 @@ ConstraintCollectors.collectAndThen(
11941187
====
11951188

11961189

1190+
[#constraintStreamsCustomCollector]
1191+
==== Implementing a custom collector
1192+
1193+
When no built-in collector covers the required aggregation,
1194+
implement `UniConstraintCollector<A, ResultContainer_, Result_>` directly
1195+
(or its `Bi...`, `Tri...`, `Quad...` counterparts for higher-arity streams).
1196+
The interface has three methods:
1197+
1198+
[cols="1,3"]
1199+
|===
1200+
| Method | Purpose
1201+
1202+
| `supplier()`
1203+
| Creates a fresh mutable result container for each group.
1204+
1205+
| `accumulator()`
1206+
| Called when a fact enters a group; returns a `UniConstraintCollectorValueHandle`
1207+
whose `add()`, `replaceWith()`, and `remove()` methods maintain the container.
1208+
1209+
| `finisher()`
1210+
| Converts the container into the immutable group result.
1211+
|===
1212+
1213+
The following example shows a simplified version of the built-in `toList()` collector:
1214+
1215+
[tabs]
1216+
====
1217+
Java::
1218+
+
1219+
[source,java,options="nowrap"]
1220+
----
1221+
public class SimpleToListCollector<A>
1222+
implements UniConstraintCollector<A, List<A>, List<A>> {
1223+
1224+
@Override
1225+
public Supplier<List<A>> supplier() {
1226+
return ArrayList::new;
1227+
}
1228+
1229+
@Override
1230+
public UniConstraintCollectorAccumulator<List<A>, A> accumulator() {
1231+
return list -> new UniConstraintCollectorValueHandle<>() {
1232+
private A current;
1233+
1234+
@Override
1235+
public void add(A element) {
1236+
current = element;
1237+
list.add(element);
1238+
}
1239+
1240+
@Override
1241+
public void remove() {
1242+
list.remove(current);
1243+
current = null;
1244+
}
1245+
};
1246+
}
1247+
1248+
@Override
1249+
public Function<List<A>, List<A>> finisher() {
1250+
return Collections::unmodifiableList;
1251+
}
1252+
}
1253+
----
1254+
====
1255+
1256+
`accumulator()` returns a factory: for each fact that enters a group,
1257+
the solver calls `intoGroup(container)` to obtain a fresh handle,
1258+
then calls `add()` exactly once, zero or more `replaceWith()` calls as the fact changes,
1259+
and `remove()` at most once when the fact leaves the group.
1260+
1261+
`list.remove(current)` removes by value (runs in linear time).
1262+
You might consider storing the insertion index instead and removing by index (runs in constant time),
1263+
but that would be risky —
1264+
any removal can shift subsequent indices at any time,
1265+
making stored positions unreliable.
1266+
Thankfully, there is a better way, using an incremental update.
1267+
1268+
When a planning variable changes, the solver calls `replaceWith()` on the affected handle.
1269+
The default implementation (inherited from `UniConstraintCollectorValueHandle`) does `remove()` followed by `add()`.
1270+
For `ArrayList`, this means scanning to find the element in linear time,
1271+
then shifting all subsequent elements to close the gap, and finally appending the new value at the end.
1272+
1273+
Overriding `replaceWith()` with `list.set()` avoids the shift entirely:
1274+
`set()` replaces the element in-place at its current position —
1275+
no elements are moved, and the element's position in the list is preserved.
1276+
1277+
[tabs]
1278+
====
1279+
Java::
1280+
+
1281+
[source,java,options="nowrap"]
1282+
----
1283+
public class IncrementalToListCollector<A>
1284+
implements UniConstraintCollector<A, List<A>, List<A>> {
1285+
1286+
@Override
1287+
public Supplier<List<A>> supplier() {
1288+
return ArrayList::new;
1289+
}
1290+
1291+
@Override
1292+
public UniConstraintCollectorAccumulator<List<A>, A> accumulator() {
1293+
return list -> new UniConstraintCollectorValueHandle<>() {
1294+
private A current;
1295+
1296+
@Override
1297+
public void add(A element) {
1298+
current = element;
1299+
list.add(element);
1300+
}
1301+
1302+
@Override
1303+
public void replaceWith(A element) {
1304+
list.set(list.indexOf(current), element);
1305+
current = element;
1306+
}
1307+
1308+
@Override
1309+
public void remove() {
1310+
list.remove(current);
1311+
current = null;
1312+
}
1313+
};
1314+
}
1315+
1316+
@Override
1317+
public Function<List<A>, List<A>> finisher() {
1318+
return Collections::unmodifiableList;
1319+
}
1320+
}
1321+
----
1322+
====
1323+
1324+
NOTE: The above implementation of `replaceWith()` is still not ideal,
1325+
as it requires a linear scan to find the element.
1326+
This will cause constraint scaling issues in larger lists,
1327+
and is the main reason why we do not recommend using `toList()` in performance-sensitive constraints.
1328+
1329+
11971330
[#constraintStreamsConditionalPropagation]
11981331
=== Conditional propagation
11991332

0 commit comments

Comments
 (0)