Repository: dubzzz/fast-check (ts)

Issue:
`set` discards too many generated values
## 🐛 Bug Report

Whenever using `fc.set` to generate random values with a minimal length specified, it discards so much generated arrays.

This could be improved to have a faster generate in those cases (minimal length >= 2).

Pull request:
⚡ Faster implementation for set
<!-- Why is this PR for? -->
<!-- Describe the reason why you opened this PR or give a link towards the associated issue. -->

## In a nutshell

Fixes #1019

❌ New feature
✔️ Fix an issue
❌ Documentation improvement
❌ Other: *please explain*

(✔️: yes, ❌: no)

## Potential impacts

Values generated by:
- set
- dictionary
- object
- anything
- json
- unicodeJson
- jsonObject
- unicodeJsonObject

Patch:
diff --git a/documentation/Arbitraries.md b/documentation/Arbitraries.md
index 175051af..f494d46d 100644
--- a/documentation/Arbitraries.md
+++ b/documentation/Arbitraries.md
@@ -993,7 +993,7 @@ fc.unicodeJson()
 // • "{\"荌鏊턳ᦖ\":false,\"냚鶖뜥\":false}"
 // • "{\"\":true,\"䷷ꔼꊐ㍂Ꮋ⧭얘\":false,\"镐菋⹥埒䘺懘ྎᶃ硾넍\":false,\"䶩လ뎙丯㷲ퟬ\":true,\"勯吓ᯇป蹥ꕪ渘Ǭ傟\":false}"
 // • "{\"\":[],\"ᐞ淙\":[]}"
-// • "{\"迵끀꧋좡ꏶ塣퐼띞\":{\"䧎﹥ï\":null},\"ቈ保婠꠨旞荫㹢ފ\":{\"콆쳑Ｈ᜞紽ѳ㑓\":false},\"\":{\"ꉶ瀞뿱끮筡팹᧊\":0.9470328025826398},\"끨\":1001562014}"
+// • "{\"迵끀꧋좡ꏶ塣퐼띞\":{\"䧎﹥ï\":null},\"ቈ保婠꠨旞荫㹢ފ\":{\"콆쳑Ｈ᜞紽ѳ㑓\":false},\"\":{\"ꉶ瀞뿱끮筡팹᧊\":0.9470328025826398},\"끨\":1001562014,\"薀ɿ⫝̸挖\":{\"顅蓦⋨뢫\":0.8487552623602803,\"ঞ\":0.7280241452947824}}"
 // • …
 
 fc.unicodeJson({maxDepth: 0})
@@ -1939,26 +1939,26 @@ fc.array(fc.nat(), {minLength: 5, maxLength: 7})
 
 ```js
 fc.set(fc.nat(99))
-// Examples of generated values: [], [15,91,64,6,44,3,85,4,0], [79], [23,39,93,87,4,85], [58,31,39,26,75]…
+// Examples of generated values: [], [15,91,64,6,44,3,4,0,85,2], [79], [23,39,93,87,85,4,21], [58,31,39,26,75]…
 
 fc.set(fc.nat(99), {maxLength: 3})
 // Examples of generated values: [], [55], [1,67,4], [12,90,43], [31]…
 
 fc.set(fc.nat(99), {minLength: 5, maxLength: 7})
-// Examples of generated values: [5,10,0,29,4,3], [53,44,67,56,49,42], [69,6,47,13,20,3,58], [3,87,23,4,0,1], [38,88,9,93,20,77,91]…
+// Examples of generated values: [5,10,0,29,4,3], [53,44,67,56,49,42], [69,6,47,13,20,3,58], [3,87,23,4,0,1], [6,1,5,28,70,4]…
 
 fc.set(fc.hexaString(), {compare: (s1, s2) => s1.length === s2.length})
 // Note: Resulting arrays will never contain two strings with the same number of characters
-// Examples of generated values: ["20","016"], [], ["447","","893c89edb1","b31a5"], ["79429d9",""], ["0","c20ea408b9","1f1574"]…
+// Examples of generated values: ["20","016"], [], ["447","","893c89edb1","b31a5"], ["79429d9",""], ["0","c20ea408b9","1f1574","117d"]…
 
 fc.set(fc.hexaString(), {minLength: 5, maxLength: 10, compare: (s1, s2) => s1.length === s2.length})
 // Note: Resulting arrays will never contain two strings with the same number of characters and it will contain between 5 and 10 strings
 // Examples of generated values:
-// • ["","18028609c9","8b9e4d","bd945ddc","7262"]
-// • ["283f273101","10f","b","ee302eda","","f3","41c2a"]
+// • ["","18028609c9","8b9e4d","bd945ddc","7262","63636"]
+// • ["7e","59ae73fd9","2d50442","182a0c3b","1e7b59","e1c1b","5"]
 // • ["65655ac0b","c20","02f6","42ff080184","80","f04e066",""]
-// • ["322","","23","2","60af6ca3"]
-// • ["","b","21a03b1","052844fe0a","7ddf1bdd","e3"]
+// • ["322","","23","2","60af6ca3","50cf","1203143b82"]
+// • ["","b","21a03b1","7ddf1bdd","e3","052844fe0a","c410"]
 // • …
 ```
 </details>
@@ -2210,8 +2210,8 @@ fc.tuple(
 fc.object()
 // Examples of generated values:
 // • {"A%":{"KFfpp":0.15601632430569612,"R;AFyG":0.5147879172882253},"":[0.031204981839495605,{"-;wLYr]a1":9007199254740991,"?HA8T":true,"V:Cmm":1026063650},true,[""]],"WFt=u'":{}}
-// • {"M{]xTH":{"#n;+\"uJ":-531067140,"#IBJPt8":[],"Bng_VbB&":[-938438981,true,1961200770,null,true]},"h(+;Pq":{"":[0,2.220446049250313e-16,2.220446049250313e-16,Number.NEGATIVE_INFINITY],"ck,":-0,"$p$ELwa4":0.665904321745444,"8Ei;%":"8vmt?65Y3j"},"-Gr":{},"(j&13ir":"sN~\\)Xb=L","":[{"Ld\"":0.5204080239001759,"zV":0.7037399006564362,"b]yOC":0.010565763906139058,"oW":0.13754518554962492,"LM[Ufcn":0.12116280558765191},true,[-9007199254740991,"bUe?2",undefined,1255384764]],"Rzdzb#:cJC":Number.POSITIVE_INFINITY,"ycMNhu2f^":{}}
-// • {"NLpz":0.3819334142270202,"`guwWVp\\q":{"OjJ U%NIw":-1412821501},"=SL35Vyp":false,"w{Q_]ZyZ":[0.5792432605379005,0.37573694944373115,0.16947480325839004],"7o5i":{"":{},"i3NvX+":{},"qH ":null,"iFv`}D)":null},"W$d#":{"e6jj":[],"j":{},"\\N52":[undefined,-407306451]},"":"a#dR85|iH","s":{"\"E;/x9w+KF":false}}
+// • {"M{]xTH":{"#n;+\"uJ":-531067140,"#IBJPt8":[],"Bng_VbB&":[-938438981,true,1961200770,null,true]},"h(+;Pq":{"":[0,2.220446049250313e-16,2.220446049250313e-16,Number.NEGATIVE_INFINITY],"ck,":-0,"$p$ELwa4":0.665904321745444,"8Ei;%":"8vmt?65Y3j"},"-Gr":{},"(j&13ir":"sN~\\)Xb=L","":[{"Ld\"":0.5204080239001759,"zV":0.7037399006564362,"b]yOC":0.010565763906139058,"oW":0.13754518554962492,"LM[Ufcn":0.12116280558765191},true,[-9007199254740991,"bUe?2",undefined,1255384764]],"Rzdzb#:cJC":Number.POSITIVE_INFINITY,"ycMNhu2f^":{},"#0viE?":1393274772,"F9{":[{"d]#*":"nq2","-eEu3t{":"e`od","BidZM=lIH":"XofQAOc","Gj6xJ":-0},0.05500515729160249,{"3@":0.6057971878524023,"]g":0.8888049320696916,"":0.20253894580064236,"u":0.9986080909514845,"-EHGp..CD":0.8372973503851951}]}
+// • {"NLpz":0.3819334142270202,"`guwWVp\\q":{"OjJ U%NIw":-1412821501},"=SL35Vyp":false,"w{Q_]ZyZ":[0.5792432605379005,0.37573694944373115,0.16947480325839004],"7o5i":{"":{},"i3NvX+":{},"qH ":null,"iFv`}D)":null,"W$d#":{"e6jj":"DjT>_><\\N","2`f1j":0.3137112850382896,"":";a#"}},"R85|iHDs+$":[null,5e-324,{" 6hP :#`Y4":0.17656752603223302,"w[JQ":0.6947822374109164}],"7bN":[[506417717,0.9439922098339762,false,-439421531],[],0.5176499969954053],"0n'+o1]0R":true}
 // • {}
 // • {"qrhd)uEl(b":{},"]fr _{^D":{"1/H-'WwF":{},"(K|5r6O":"tDi'?MuF"},"H":null}
 // • …
@@ -2224,7 +2224,7 @@ fc.object({
 // • {"c":{"c":"DIxqj35h9:","a":"#P'DE&gY","b":"F"}}
 // • {"b":-25}
 // • {"b":false,"c":undefined}
-// • {"a":4.872535319666582e-8,"b":{"c":"$Cf%"}}
+// • {"a":4.872535319666582e-8,"b":{"c":"$Cf%"},"c":""}
 // • {}
 // • …
 
@@ -2234,7 +2234,7 @@ fc.object({
 // Examples of generated values:
 // • {"^lx)`P":0,"</}}e{{":0.3250894455517638,"g21/@#y1B":-0,"8ULm U|p<":Number.NEGATIVE_INFINITY,"mrI#":Number.POSITIVE_INFINITY,"{sS8U7 %E!":2089101388,"=(":0.10812626313097728}
 // • {"s!?U&|m":" !","m}7P4>bQM?":null}
-// • {":WEs/srS+":-1416520206,"lb(<%.BW9":1.7976931348623157e+308,"":true,"pRw":null,"PfnX>":0.08774441941808431}
+// • {":WEs/srS+":-1416520206,"lb(<%.BW9":1.7976931348623157e+308,"":true,"PfnX>":0.08774441941808431,"pRw":null,"o};9PoD":"I[r7TL+"}
 // • {"WW!oe%r(1":"FiY","l":0.03574426867100822,"R@~l-":false,"":1980690840,"E:' snhE\"}":" <4QOmI","=k]:kN3b~6":1139673390}
 // • {"y.&&$":-9007199254740991}
 // • …
@@ -2263,7 +2263,7 @@ fc.object({
 // Examples of generated values:
 // • {"F,r0fx}OWg":[new Date("+160385-06-21T16:52:36.736Z"),22583622805204069477977266286996933943873259784384626171039405815620425326468n,{"{Y":"N",",;04yN":-107026440}],"6x*pJp&3^":"new Number(0.42255506544021615)"}
 // • {}
-// • {"hyu)K.|R-5":"new Set([new String(\"f0q}b8/2*\"),new String(\" scrLAI'KD\"),new String(\"P_krOW\"),new String(\"hZ\"),new String(\"VwvEE}s(;V\")])","85c":1955536278377593253161285446750593118355413969436917489136958056479360825003n,"Y*":new Number(0.4145363362551938),"[n7%b":"new Number(0.861563135058088)","L":-36290478173422814553429011041239932856189046009049526980429727311354359757843n,"qe80E%":new Set([undefined,null,new String("-:"),new String("@}gz6")]),"6f*":new Set([]),"8t":{"%*b;)f":Object.assign(Object.create(null),{"vr":false,"e67K#h_N>":null})," 5yAgx":"<-"},"K":new Date("+121904-08-26T01:40:50.304Z")}
+// • {"hyu)K.|R-5":"new Set([new String(\"f0q}b8/2*\"),new String(\" scrLAI'KD\"),new String(\"P_krOW\"),new String(\"hZ\"),new String(\"VwvEE}s(;V\")])","85c":1955536278377593253161285446750593118355413969436917489136958056479360825003n,"Y*":new Number(0.4145363362551938),"[n7%b":"new Number(0.861563135058088)","L":-36290478173422814553429011041239932856189046009049526980429727311354359757843n,"qe80E%":new Set([undefined,new String("-:"),new String("@}gz6"),null,new String("f*;+=I8t")]),"":new Set([new Number(0.8938732846739627),new Number(0.5945807356752961)]),"fvH0g*vr":Object.create(null),"e67K#h_N>":Object.assign(Object.create(null),{"5yAgxxIc":null})}
 // • {"r5B":Object.assign(Object.create(null),{"":false,"new Set([new Boolean(false),new Boolean(true)])":true,"G $D&  %#x":true}),"":{"-47n":1387385502,"POG^":15},"lf*":49n,"UN":new Date("1969-12-31T23:59:59.948Z")}
 // • {")h%{":new Set([])}
 // • …
@@ -2389,8 +2389,8 @@ fc.anything({
 // Examples of generated values:
 // • {"b":7.12556398330122e-8}
 // • [["(y","\"a>%","5AM2+"],"$%",{"c":Number.NaN,"b":1.708876607997567e-7}]
-// • {"b":1104845694,"c":1617404783}
-// • {"c":-694807229,"b":-1525211291}
+// • {"b":1104845694,"c":1617404783,"a":-86638758}
+// • {"c":-694807229,"b":-1525211291,"a":-97094564}
 // • 5e-324
 // • …
 
diff --git a/src/check/arbitrary/ArrayArbitrary.ts b/src/check/arbitrary/ArrayArbitrary.ts
index 40abe52a..391b76e7 100644
--- a/src/check/arbitrary/ArrayArbitrary.ts
+++ b/src/check/arbitrary/ArrayArbitrary.ts
@@ -7,18 +7,24 @@ import { biasWrapper } from './definition/BiasedArbitraryWrapper';
 import { Shrinkable } from './definition/Shrinkable';
 import { integer } from './IntegerArbitrary';
 import { makeLazy } from '../../stream/LazyIterableIterator';
+import { buildCompareFilter } from './helpers/BuildCompareFilter';
 
 /** @internal */
 export class ArrayArbitrary<T> extends Arbitrary<T[]> {
   readonly lengthArb: ArbitraryWithShrink<number>;
+  readonly preFilter: (tab: Shrinkable<T>[]) => Shrinkable<T>[];
+
   constructor(
     readonly arb: Arbitrary<T>,
     readonly minLength: number,
     readonly maxLength: number,
-    readonly preFilter: (tab: Shrinkable<T>[]) => Shrinkable<T>[] = (tab) => tab
+    // Whenever passing a isEqual to ArrayArbitrary, you also have to filter
+    // it's output just in case produced values are too small (below minLength)
+    readonly isEqual?: (valueA: T, valueB: T) => boolean
   ) {
     super();
     this.lengthArb = integer(minLength, maxLength);
+    this.preFilter = this.isEqual !== undefined ? buildCompareFilter(this.isEqual) : (tab: Shrinkable<T>[]) => tab;
   }
   private static makeItCloneable<T>(vs: T[], shrinkables: Shrinkable<T>[]) {
     (vs as any)[cloneMethod] = () => {
@@ -31,8 +37,21 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
     };
     return vs;
   }
+  private canAppendItem(items: Shrinkable<T>[], newItem: Shrinkable<T>): boolean {
+    if (this.isEqual === undefined) {
+      return true;
+    }
+    for (let idx = 0; idx !== items.length; ++idx) {
+      if (this.isEqual(items[idx].value_, newItem.value_)) {
+        return false;
+      }
+    }
+    return true;
+  }
   private wrapper(itemsRaw: Shrinkable<T>[], shrunkOnce: boolean): Shrinkable<T[]> {
-    const items = this.preFilter(itemsRaw);
+    // We need to explicitly apply filtering on shrink items
+    // has they might have duplicates (on non shrunk it is not the case by construct)
+    const items = shrunkOnce ? this.preFilter(itemsRaw) : itemsRaw;
     let cloneable = false;
     const vs = [];
     for (let idx = 0; idx !== items.length; ++idx) {
@@ -46,10 +65,24 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
     return new Shrinkable(vs, () => this.shrinkImpl(items, shrunkOnce).map((v) => this.wrapper(v, true)));
   }
   generate(mrng: Random): Shrinkable<T[]> {
-    const size = this.lengthArb.generate(mrng);
+    const targetSizeShrinkable = this.lengthArb.generate(mrng);
+    const targetSize = targetSizeShrinkable.value;
+
+    let numSkippedInRow = 0;
     const items: Shrinkable<T>[] = [];
-    for (let idx = 0; idx !== size.value; ++idx) {
-      items.push(this.arb.generate(mrng));
+    // Try to append into items up to the target size
+    // In the case of a set we may reject some items as they are already part of the set
+    // so we need to retry and generate other ones. In order to prevent infinite loop,
+    // we accept a max of maxLength consecutive failures. This circuit breaker may cause
+    // generated to be smaller than the minimal accepted one.
+    while (items.length < targetSize && numSkippedInRow < this.maxLength) {
+      const current = this.arb.generate(mrng);
+      if (this.canAppendItem(items, current)) {
+        numSkippedInRow = 0;
+        items.push(current);
+      } else {
+        numSkippedInRow += 1;
+      }
     }
     return this.wrapper(items, false);
   }
@@ -78,7 +111,7 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
         originalArbitrary.arb.withBias(freq),
         originalArbitrary.minLength,
         originalArbitrary.maxLength,
-        originalArbitrary.preFilter
+        originalArbitrary.isEqual
       );
       const highBiasedArbBuilder = () => {
         return originalArbitrary.minLength !== originalArbitrary.maxLength
@@ -87,13 +120,13 @@ export class ArrayArbitrary<T> extends Arbitrary<T[]> {
               originalArbitrary.minLength,
               originalArbitrary.minLength +
                 Math.floor(Math.log(originalArbitrary.maxLength - originalArbitrary.minLength) / Math.log(2)),
-              originalArbitrary.preFilter
+              originalArbitrary.isEqual
             )
           : new ArrayArbitrary(
               originalArbitrary.arb.withBias(freq),
               originalArbitrary.minLength,
               originalArbitrary.maxLength,
-              originalArbitrary.preFilter
+              originalArbitrary.isEqual
             );
       };
       return biasWrapper(freq, lowBiased, highBiasedArbBuilder);
diff --git a/src/check/arbitrary/SetArbitrary.ts b/src/check/arbitrary/SetArbitrary.ts
index 8a076301..b7528a5b 100644
--- a/src/check/arbitrary/SetArbitrary.ts
+++ b/src/check/arbitrary/SetArbitrary.ts
@@ -1,35 +1,5 @@
 import { ArrayArbitrary, maxLengthFromMinLength } from './ArrayArbitrary';
 import { Arbitrary } from './definition/Arbitrary';
-import { Shrinkable } from './definition/Shrinkable';
-
-/** @internal */
-function subArrayContains<T>(tab: T[], upperBound: number, includeValue: (v: T) => boolean): boolean {
-  for (let idx = 0; idx < upperBound; ++idx) {
-    if (includeValue(tab[idx])) return true;
-  }
-  return false;
-}
-
-/** @internal */
-function swap<T>(tab: T[], idx1: number, idx2: number): void {
-  const temp = tab[idx1];
-  tab[idx1] = tab[idx2];
-  tab[idx2] = temp;
-}
-
-/** @internal */
-export function buildCompareFilter<T>(compare: (a: T, b: T) => boolean): (tab: Shrinkable<T>[]) => Shrinkable<T>[] {
-  return (tab: Shrinkable<T>[]): Shrinkable<T>[] => {
-    let finalLength = tab.length;
-    for (let idx = tab.length - 1; idx !== -1; --idx) {
-      if (subArrayContains(tab, idx, (t) => compare(t.value_, tab[idx].value_))) {
-        --finalLength;
-        swap(tab, idx, finalLength);
-      }
-    }
-    return tab.slice(0, finalLength);
-  };
-}
 
 /**
  * Build fully set SetConstraints from a partial data
@@ -199,7 +169,7 @@ function set<T>(
   const maxLength = constraints.maxLength;
   const compare = constraints.compare;
 
-  const arrayArb = new ArrayArbitrary<T>(arb, minLength, maxLength, buildCompareFilter(compare));
+  const arrayArb = new ArrayArbitrary<T>(arb, minLength, maxLength, compare);
   if (minLength === 0) return arrayArb;
   return arrayArb.filter((tab) => tab.length >= minLength);
 }
diff --git a/src/check/arbitrary/helpers/BuildCompareFilter.ts b/src/check/arbitrary/helpers/BuildCompareFilter.ts
new file mode 100644
index 00000000..886b1a91
--- /dev/null
+++ b/src/check/arbitrary/helpers/BuildCompareFilter.ts
@@ -0,0 +1,37 @@
+import { Shrinkable } from '../definition/Shrinkable';
+
+/** @internal */
+function subArrayContains<T>(tab: T[], upperBound: number, includeValue: (v: T) => boolean): boolean {
+  for (let idx = 0; idx < upperBound; ++idx) {
+    if (includeValue(tab[idx])) return true;
+  }
+  return false;
+}
+
+/** @internal */
+function swap<T>(tab: T[], idx1: number, idx2: number): void {
+  const temp = tab[idx1];
+  tab[idx1] = tab[idx2];
+  tab[idx2] = temp;
+}
+
+/**
+ * Build a function that will be used to remove all values that are equal given
+ * the comparison function compare
+ *
+ * The produced function takes an array of items as input and alter it (swap items).
+ * It returns the array without any duplicates.
+ * @internal
+ */
+export function buildCompareFilter<T>(compare: (a: T, b: T) => boolean): (tab: Shrinkable<T>[]) => Shrinkable<T>[] {
+  return (tab: Shrinkable<T>[]): Shrinkable<T>[] => {
+    let finalLength = tab.length;
+    for (let idx = tab.length - 1; idx !== -1; --idx) {
+      if (subArrayContains(tab, idx, (t) => compare(t.value_, tab[idx].value_))) {
+        --finalLength;
+        swap(tab, idx, finalLength);
+      }
+    }
+    return tab.slice(0, finalLength);
+  };
+}


Test patch:
diff --git a/test/unit/check/arbitrary/SetArbitrary.spec.ts b/test/unit/check/arbitrary/SetArbitrary.spec.ts
index 3518b4b9..24203ec5 100644
--- a/test/unit/check/arbitrary/SetArbitrary.spec.ts
+++ b/test/unit/check/arbitrary/SetArbitrary.spec.ts
@@ -1,7 +1,6 @@
 import * as fc from '../../../../lib/fast-check';
 
-import { Shrinkable } from '../../../../src/check/arbitrary/definition/Shrinkable';
-import { buildCompareFilter, set } from '../../../../src/check/arbitrary/SetArbitrary';
+import { set } from '../../../../src/check/arbitrary/SetArbitrary';
 import { nat } from '../../../../src/check/arbitrary/IntegerArbitrary';
 import { string } from '../../../../src/check/arbitrary/StringArbitrary';
 
@@ -34,17 +33,6 @@ const isStrictlySmallerCustomSet = (arr1: { key: number }[], arr2: { key: number
   );
 
 describe('SetArbitrary', () => {
-  describe('buildCompareFilter', () => {
-    it('Should filter array from duplicated values', () =>
-      fc.assert(
-        fc.property(fc.array(fc.nat()), (tab) => {
-          const filter = buildCompareFilter<number>((a, b) => a === b);
-          const adaptedTab = tab.map((v) => new Shrinkable(v));
-          const filteredTab = filter(adaptedTab);
-          expect(validSet(filteredTab.map((s) => s.value))).toBe(true);
-        })
-      ));
-  });
   describe('set', () => {
     describe('Given no length constraints [unique items only]', () => {
       genericHelper.isValidArbitrary(() => set(nat()), {
diff --git a/test/unit/check/arbitrary/helpers/BuildCompareFilter.spec.ts b/test/unit/check/arbitrary/helpers/BuildCompareFilter.spec.ts
new file mode 100644
index 00000000..08b3ec23
--- /dev/null
+++ b/test/unit/check/arbitrary/helpers/BuildCompareFilter.spec.ts
@@ -0,0 +1,19 @@
+import * as fc from '../../../../../lib/fast-check';
+
+import { Shrinkable } from '../../../../../src/check/arbitrary/definition/Shrinkable';
+
+import { buildCompareFilter } from '../../../../../src/check/arbitrary/helpers/BuildCompareFilter';
+
+const validSet = (s: number[]) => s.length === new Set(s).size && s.every((e) => typeof e === 'number');
+
+describe('buildCompareFilter', () => {
+  it('Should filter array from duplicated values', () =>
+    fc.assert(
+      fc.property(fc.array(fc.nat()), (tab) => {
+        const filter = buildCompareFilter<number>((a, b) => a === b);
+        const adaptedTab = tab.map((v) => new Shrinkable(v));
+        const filteredTab = filter(adaptedTab);
+        expect(validSet(filteredTab.map((s) => s.value))).toBe(true);
+      })
+    ));
+});