Repository: dubzzz/fast-check
Pull request #1701 (2021-04-06 20:37:53)

♻️ Migrate memo to NextArbitrary
<!-- Why is this PR for? -->
<!-- Describe the reason why you opened this PR or give a link towards the associated issue. -->

## In a nutshell

❌ New feature
❌ Fix an issue
❌ Documentation improvement
✔️ Other: *please explain*

(✔️: yes, ❌: no)

## Potential impacts

None expected 🤞 

But if any, some on `memo` regarding:
- Generated values impact,
- Shrink values impact

codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1701/builds/116961) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 2078eb138058e933b8f2698244a41276debeb239:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-gu2hu)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-ipmsm)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1701?src=pr&el=h1) Report
> Merging [#1701](https://codecov.io/gh/dubzzz/fast-check/pull/1701?src=pr&el=desc) (2078eb1) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/45163e051a1ba10f02c1ff17c322f958874bab34?el=desc) (45163e0) will **decrease** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1701/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730)](https://codecov.io/gh/dubzzz/fast-check/pull/1701?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main    #1701      +/-   ##
==========================================
- Coverage   97.17%   97.17%   -0.01%     
==========================================
  Files          98       98              
  Lines        3504     3502       -2     
  Branches      684      683       -1     
==========================================
- Hits         3405     3403       -2     
  Misses         95       95              
  Partials        4        4              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `97.17% <100.00%> (-0.01%)` | :arrow_down: |
| unit-tests-10.x-Linux | `97.17% <100.00%> (-0.01%)` | :arrow_down: |
| unit-tests-12.x-Linux | `97.17% <100.00%> (-0.01%)` | :arrow_down: |
| unit-tests-14.x-Linux | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dubzzz/fast-check/pull/1701?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/check/arbitrary/MemoArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1701/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9NZW1vQXJiaXRyYXJ5LnRz) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1701?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1701?src=pr&el=footer). Last update [45163e0...2078eb1](https://codecov.io/gh/dubzzz/fast-check/pull/1701?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/check/arbitrary/MemoArbitrary.ts b/src/check/arbitrary/MemoArbitrary.ts
index 0e0e5cbd..1abc83b9 100644
--- a/src/check/arbitrary/MemoArbitrary.ts
+++ b/src/check/arbitrary/MemoArbitrary.ts
@@ -1,23 +1,23 @@
+import { Stream } from '../../fast-check-default';
 import { Random } from '../../random/generator/Random';
 import { Arbitrary } from './definition/Arbitrary';
-import { Shrinkable } from './definition/Shrinkable';
+import { convertFromNext, convertToNext } from './definition/Converters';
+import { NextArbitrary } from './definition/NextArbitrary';
+import { NextValue } from './definition/NextValue';
 
 /** @internal */
-export class MemoArbitrary<T> extends Arbitrary<T> {
-  private lastFreq = -1;
-  private lastBiased: Arbitrary<T> = this;
-  constructor(readonly underlying: Arbitrary<T>) {
+export class MemoArbitrary<T> extends NextArbitrary<T> {
+  constructor(readonly underlying: NextArbitrary<T>) {
     super();
   }
-  generate(mrng: Random): Shrinkable<T> {
-    return this.underlying.generate(mrng);
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<T> {
+    return this.underlying.generate(mrng, biasFactor);
   }
-  withBias(freq: number): Arbitrary<T> {
-    if (freq !== this.lastFreq) {
-      this.lastFreq = freq;
-      this.lastBiased = this.underlying.withBias(freq);
-    }
-    return this.lastBiased;
+  canGenerate(value: unknown): value is T {
+    return this.underlying.canGenerate(value);
+  }
+  shrink(value: T, context?: unknown): Stream<NextValue<T>> {
+    return this.underlying.shrink(value, context);
   }
 }
 
@@ -57,7 +57,7 @@ export function memo<T>(builder: (maxDepth: number) => Arbitrary<T>): Memo<T> {
     if (!Object.prototype.hasOwnProperty.call(previous, n)) {
       const prev = contextRemainingDepth;
       contextRemainingDepth = n - 1;
-      previous[n] = new MemoArbitrary(builder(n));
+      previous[n] = convertFromNext(new MemoArbitrary(convertToNext(builder(n))));
       contextRemainingDepth = prev;
     }
     return previous[n];


Test patch:
diff --git a/test/unit/check/arbitrary/MemoArbitrary.spec.ts b/test/unit/check/arbitrary/MemoArbitrary.spec.ts
deleted file mode 100644
index befde076..00000000
--- a/test/unit/check/arbitrary/MemoArbitrary.spec.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { MemoArbitrary, memo } from '../../../../src/check/arbitrary/MemoArbitrary';
-import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
-import { Shrinkable } from '../../../../src/check/arbitrary/definition/Shrinkable';
-import { Random } from '../../../../src/random/generator/Random';
-
-import * as stubRng from '../../stubs/generators';
-
-describe('MemoArbitrary', () => {
-  describe('memo', () => {
-    it('Should wrap the arbitrary into a MemoArbitrary', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const builder = memo(() => expectedArb);
-      const arb = builder();
-
-      expect(arb).toBeInstanceOf(MemoArbitrary);
-      expect(((arb as any) as MemoArbitrary<any>).underlying).toBe(expectedArb);
-    });
-    it('Should cache arbitraries associated to each depth', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const builder = memo(() => expectedArb);
-      const arb = builder();
-
-      expect(builder(10)).toBe(builder(10));
-      expect(builder(42)).toBe(builder(42));
-      expect(builder(65500)).toBe(builder(65500));
-      expect(builder()).toBe(arb);
-    });
-    it('Should instantiate new arbitraries for each depth', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const builder = memo(() => expectedArb);
-
-      expect(builder(10)).not.toBe(builder(42));
-    });
-    it('Should consider no depth as depth 10', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const builder = memo(() => expectedArb);
-
-      expect(builder()).toBe(builder(10));
-    });
-    it('Should automatically decrease depth for self recursive', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const memoFun = jest.fn();
-      memoFun.mockImplementation((n) => (n <= 6 ? expectedArb : builder()));
-      const builder = memo(memoFun);
-
-      builder();
-
-      expect(memoFun.mock.calls).toEqual([[10], [9], [8], [7], [6]]);
-    });
-    it('Should automatically interleave decrease depth for mutually recursive', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const memoFunA = jest.fn();
-      memoFunA.mockImplementation((n) => (n <= 6 ? expectedArb : builderB()));
-      const memoFunB = jest.fn();
-      memoFunB.mockImplementation((n) => (n <= 6 ? expectedArb : builderA()));
-      const builderA = memo(memoFunA);
-      const builderB = memo(memoFunB);
-
-      builderA();
-
-      expect(memoFunA.mock.calls).toEqual([[10], [8], [6]]);
-      expect(memoFunB.mock.calls).toEqual([[9], [7]]);
-    });
-    it('Should be able to override decrease depth', () => {
-      const expectedArb = buildArbitrary(jest.fn());
-      const memoFun = jest.fn();
-      memoFun.mockImplementation((n) => (n <= 0 ? expectedArb : builder(n - 3)));
-      const builder = memo(memoFun);
-
-      builder();
-
-      expect(memoFun.mock.calls).toEqual([[10], [7], [4], [1], [-2]]);
-    });
-    it('Should be able to delay calls to sub-builders', () => {
-      const mrng = stubRng.mutable.nocall();
-      const generateMock = jest.fn();
-      const simpleArb = buildArbitrary(generateMock);
-      const builderA = memo(() => buildArbitrary((mrng) => builderB().generate(mrng)));
-      const builderB = memo(() => simpleArb);
-
-      expect(generateMock).not.toHaveBeenCalled();
-      builderA().generate(mrng);
-
-      expect(generateMock).toHaveBeenCalled();
-    });
-  });
-  describe('MemoArbitrary', () => {
-    it('Should call generate method of underlying on generate', () => {
-      const mrng = stubRng.mutable.nocall();
-      const expectedGen = Symbol();
-      const generateMock = jest.fn();
-      generateMock.mockReturnValue(expectedGen);
-      const arb = buildArbitrary(generateMock);
-      const memoArb = new MemoArbitrary(arb);
-
-      const g = memoArb.generate(mrng);
-      expect(g).toBe(expectedGen);
-      expect(generateMock).toHaveBeenCalledTimes(1);
-    });
-    it('Should be able to bias to the biased value of underlying', () => {
-      const noCallMock = jest.fn();
-      const biasedArb = buildArbitrary(noCallMock);
-      const arb = buildArbitrary(noCallMock, () => biasedArb);
-      const memoArb = new MemoArbitrary(arb);
-
-      const biasedLazy = memoArb.withBias(2);
-      expect(biasedLazy).toBe(biasedArb);
-      expect(noCallMock).not.toHaveBeenCalled();
-    });
-    it('Should cache biased arbitrary for same freq', () => {
-      const biasArb48 = buildArbitrary(jest.fn());
-      const biasMock = jest.fn();
-      biasMock.mockImplementationOnce(() => biasArb48);
-      const arb = buildArbitrary(jest.fn(), biasMock);
-      const memoArb = new MemoArbitrary(arb);
-
-      memoArb.withBias(48);
-      biasMock.mockClear();
-
-      expect(memoArb.withBias(48)).toBe(biasArb48);
-      expect(biasMock).not.toBeCalled();
-    });
-    it('Should not take from cache if freq changed', () => {
-      const biasArb48 = buildArbitrary(jest.fn());
-      const biasArb69 = buildArbitrary(jest.fn());
-      const biasMock = jest.fn();
-      biasMock.mockImplementationOnce(() => biasArb48).mockImplementationOnce(() => biasArb69);
-      const arb = buildArbitrary(jest.fn(), biasMock);
-      const memoArb = new MemoArbitrary(arb);
-
-      memoArb.withBias(48);
-      biasMock.mockClear();
-
-      expect(memoArb.withBias(69)).toBe(biasArb69);
-      expect(biasMock).toBeCalled();
-    });
-  });
-});
-
-const buildArbitrary = (generate: (mrng: Random) => Shrinkable<any>, withBias?: (n: number) => Arbitrary<any>) => {
-  return new (class extends Arbitrary<any> {
-    generate(mrng: Random) {
-      return generate(mrng);
-    }
-    withBias(n: number): Arbitrary<any> {
-      return withBias ? withBias(n) : this;
-    }
-  })();
-};
diff --git a/test/unit/check/arbitrary/MemoArbitrary.utest.spec.ts b/test/unit/check/arbitrary/MemoArbitrary.utest.spec.ts
new file mode 100644
index 00000000..315c3488
--- /dev/null
+++ b/test/unit/check/arbitrary/MemoArbitrary.utest.spec.ts
@@ -0,0 +1,197 @@
+import { MemoArbitrary, memo as memoOld } from '../../../../src/check/arbitrary/MemoArbitrary';
+import { NextArbitrary } from '../../../../src/check/arbitrary/definition/NextArbitrary';
+import { convertFromNext, convertToNext } from '../../../../src/check/arbitrary/definition/Converters';
+import { NextValue } from '../../../../src/check/arbitrary/definition/NextValue';
+import { Stream } from '../../../../src/stream/Stream';
+
+import { fakeNextArbitrary } from './generic/NextArbitraryHelpers';
+import * as stubRng from '../../stubs/generators';
+
+type NextMemo<T> = (maxDepth?: number) => NextArbitrary<T>;
+
+const mrngNoCall = stubRng.mutable.nocall();
+function memo<T>(builder: (maxDepth: number) => NextArbitrary<T>): NextMemo<T> {
+  const memoed = memoOld((d) => convertFromNext(builder(d)));
+  return (maxDepth?: number) => convertToNext(memoed(maxDepth));
+}
+
+describe('memo', () => {
+  describe('builder', () => {
+    it('should wrap the arbitrary into a MemoArbitrary', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+      const builder = memo(() => expectedArb);
+
+      // Act
+      const arb = builder();
+
+      // Assert
+      expect(arb).toBeInstanceOf(MemoArbitrary);
+      expect(((arb as any) as MemoArbitrary<any>).underlying).toBe(expectedArb);
+    });
+
+    it('should cache arbitraries associated to each depth', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+      const builder = memo(() => expectedArb);
+
+      // Act
+      const arb = builder();
+
+      // Assert
+      expect(builder(10)).toBe(builder(10));
+      expect(builder(42)).toBe(builder(42));
+      expect(builder(65500)).toBe(builder(65500));
+      expect(builder()).toBe(arb);
+    });
+
+    it('should instantiate new arbitraries for each depth', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+      const builder = memo(() => expectedArb);
+
+      // Act
+      const b10 = builder(10);
+      const b42 = builder(42);
+
+      // Assert
+      expect(b10).not.toBe(b42);
+    });
+
+    it('should consider no depth as depth 10', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+      const builder = memo(() => expectedArb);
+
+      // Act
+      const bDefault = builder();
+      const b10 = builder(10);
+
+      // Assert
+      expect(bDefault).toBe(b10);
+    });
+
+    it('should automatically decrease depth for self recursive', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+      const memoFun = jest.fn();
+      const builder = memo(memoFun);
+      memoFun.mockImplementation((n) => (n <= 6 ? expectedArb : builder()));
+
+      // Act
+      builder();
+
+      // Assert
+      expect(memoFun.mock.calls).toEqual([[10], [9], [8], [7], [6]]);
+    });
+
+    it('should automatically interleave decrease depth for mutually recursive', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+      const memoFunA = jest.fn();
+      const memoFunB = jest.fn();
+      const builderA = memo(memoFunA);
+      const builderB = memo(memoFunB);
+      memoFunA.mockImplementation((n) => (n <= 6 ? expectedArb : builderB()));
+      memoFunB.mockImplementation((n) => (n <= 6 ? expectedArb : builderA()));
+
+      // Act
+      builderA();
+
+      // Assert
+      expect(memoFunA.mock.calls).toEqual([[10], [8], [6]]);
+      expect(memoFunB.mock.calls).toEqual([[9], [7]]);
+    });
+
+    it('should be able to override decrease depth', () => {
+      // Arrange
+      const { instance: expectedArb } = fakeNextArbitrary();
+      const memoFun = jest.fn();
+      const builder = memo(memoFun);
+      memoFun.mockImplementation((n) => (n <= 0 ? expectedArb : builder(n - 3)));
+
+      // Act
+      builder();
+
+      // Assert
+      expect(memoFun.mock.calls).toEqual([[10], [7], [4], [1], [-2]]);
+    });
+
+    it('should be able to delay calls to sub-builders', () => {
+      // Arrange
+      const expectedBiasedFactor = 42;
+      const { instance: simpleArb, generate } = fakeNextArbitrary();
+
+      // Act
+      const builderA = memo(() => {
+        const { instance: arbA, generate: generateA } = fakeNextArbitrary();
+        generateA.mockImplementation((mrng, biasedFactor) => builderB().generate(mrng, biasedFactor));
+        return arbA;
+      });
+      const builderB = memo(() => simpleArb);
+      expect(generate).not.toHaveBeenCalled();
+      builderA().generate(mrngNoCall, expectedBiasedFactor);
+
+      // Assert
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, expectedBiasedFactor);
+    });
+  });
+
+  describe('generate', () => {
+    it('should call generate method of underlying on generate', () => {
+      // Arrange
+      const expectedBiasedFactor = 2;
+      const expectedGen = Symbol();
+      const expectedNextValue = new NextValue(expectedGen);
+      const { instance: arb, generate } = fakeNextArbitrary();
+      generate.mockReturnValue(expectedNextValue);
+
+      // Act
+      const memoArb = new MemoArbitrary(arb);
+      const g = memoArb.generate(mrngNoCall, expectedBiasedFactor);
+
+      // Assert
+      expect(g).toBe(expectedNextValue);
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, expectedBiasedFactor);
+    });
+  });
+
+  describe('canGenerate', () => {
+    it('should call canGenerate method of underlying on canGenerate', () => {
+      // Arrange
+      const expectedValue = Symbol();
+      const { instance: arb, canGenerate } = fakeNextArbitrary();
+      canGenerate.mockReturnValue(true);
+
+      // Act
+      const memoArb = new MemoArbitrary(arb);
+      const out = memoArb.canGenerate(expectedValue);
+
+      // Assert
+      expect(out).toBe(true);
+      expect(canGenerate).toHaveBeenCalledTimes(1);
+      expect(canGenerate).toHaveBeenCalledWith(expectedValue);
+    });
+  });
+
+  describe('shrink', () => {
+    it('should call shrink method of underlying on shrink', () => {
+      // Arrange
+      const expectedValue = Symbol();
+      const expectedContext = Symbol();
+      const expectedShrinks = Stream.of(new NextValue(Symbol()));
+      const { instance: arb, shrink } = fakeNextArbitrary();
+      shrink.mockReturnValue(expectedShrinks);
+
+      // Act
+      const memoArb = new MemoArbitrary(arb);
+      const out = memoArb.shrink(expectedValue, expectedContext);
+
+      // Assert
+      expect(out).toBe(expectedShrinks);
+      expect(shrink).toHaveBeenCalledTimes(1);
+      expect(shrink).toHaveBeenCalledWith(expectedValue, expectedContext);
+    });
+  });
+});