From 905b1370c2b3a39f51274a052bc9937ef7ed5d0c Mon Sep 17 00:00:00 2001 From: Dierk Koenig Date: Thu, 7 Sep 2023 17:21:03 +0200 Subject: [PATCH] feature: add "unfold" ctor and make Sequence use it as the implementation. --- docs/src/allTests.html | 2 +- .../sequence/constructors/constructorTest.js | 1 + .../sequence/constructors/constructors.js | 1 + .../constructors/sequence/Sequence.js | 28 +++------- .../sequence/constructors/unfold/unfold.js | 53 +++++++++++++++++++ .../constructors/unfold/unfoldTest.js | 25 +++++++++ 6 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 docs/src/kolibri/sequence/constructors/unfold/unfold.js create mode 100644 docs/src/kolibri/sequence/constructors/unfold/unfoldTest.js diff --git a/docs/src/allTests.html b/docs/src/allTests.html index 7abda535..9747a343 100644 --- a/docs/src/allTests.html +++ b/docs/src/allTests.html @@ -12,7 +12,7 @@

All Tests Report

-

1244 test(s) expected.

+

1247 test(s) expected.

Check console for possible errors.

diff --git a/docs/src/kolibri/sequence/constructors/constructorTest.js b/docs/src/kolibri/sequence/constructors/constructorTest.js index 3f918180..7a24cf71 100644 --- a/docs/src/kolibri/sequence/constructors/constructorTest.js +++ b/docs/src/kolibri/sequence/constructors/constructorTest.js @@ -7,3 +7,4 @@ import "./seq/seqTest.js"; import "./stackSequence/stackSequenceTest.js"; import "./tupleSequence/tupleSequenceTest.js"; import "./range/rangeTest.js"; +import "./unfold/unfoldTest.js"; diff --git a/docs/src/kolibri/sequence/constructors/constructors.js b/docs/src/kolibri/sequence/constructors/constructors.js index 1d923f35..e84fe6cf 100644 --- a/docs/src/kolibri/sequence/constructors/constructors.js +++ b/docs/src/kolibri/sequence/constructors/constructors.js @@ -7,3 +7,4 @@ export * from "./seq/seq.js"; export * from "./replicate/replicate.js"; export * from "./repeat/repeat.js"; export * from "./range/range.js"; +export * from "./unfold/unfold.js"; diff --git a/docs/src/kolibri/sequence/constructors/sequence/Sequence.js b/docs/src/kolibri/sequence/constructors/sequence/Sequence.js index 7fb8d139..2ea0990d 100644 --- a/docs/src/kolibri/sequence/constructors/sequence/Sequence.js +++ b/docs/src/kolibri/sequence/constructors/sequence/Sequence.js @@ -1,4 +1,4 @@ -import { createMonadicSequence } from "../../util/sequenceUtil/createMonadicSequence.js"; +import { unfold } from "../unfold/unfold.js"; export { Sequence } @@ -28,24 +28,12 @@ export { Sequence } * // => Logs '0, 1, 2' */ -const Sequence = (start, whileFunction, incrementFunction) => { +const Sequence = (start, whileFunction, incrementFunction) => - const iterator = () => { - let value = start; - /** - * @template _T_ - * Returns the next iteration of this iterable object. - * @returns { IteratorResult<_T_, _T_> } - */ - const next = () => { - const current = value; - const done = !whileFunction(current); - if (!done) value = incrementFunction(value); - return { done, value: current }; - }; + unfold( + start, + current => whileFunction(current) + ? { state: incrementFunction(current), value: current } + : undefined + ); - return { next }; - }; - - return createMonadicSequence(iterator); -}; diff --git a/docs/src/kolibri/sequence/constructors/unfold/unfold.js b/docs/src/kolibri/sequence/constructors/unfold/unfold.js new file mode 100644 index 00000000..3a4c7eb8 --- /dev/null +++ b/docs/src/kolibri/sequence/constructors/unfold/unfold.js @@ -0,0 +1,53 @@ +/** + * @module kolibri.sequence.constructors.unfold + * The idea was thankfully provided by Daniel Kröni. + */ +import {createMonadicSequence} from "../../util/sequenceUtil/createMonadicSequence.js"; + +export { unfold } +/** + * @typedef StateAndValueType + * @template _S_, _T_ + * @property { _S_ } state + * @property { _T_ } value + */ + +/** + * @typedef FromStateToNextStateAndValue + * @callback + * @pure The whole idea of `unfold` is to allow a pure function at this point. Depends on developer discipline. + * @template _S_, _T_ + * @param { _S_ } state + * @return { StateAndValueType<_S_, _T_> | undefined } - `undefined` if the sequence cannot produce any more values + */ + +/** + * Creates a {@link SequenceType} from a callback function that generates the next state and value from the current state. + * The sequence is supposed to be exhausted when the callback returns `undefined`. + * `unfold` abstracts over the proper state management + * in the closure scope of an {@link Iterable}'s iterator function. + * This allows the {@link FromStateToNextStateAndValue} callback function to be pure. + * @template _T_, _S_ + * @param { FromStateToNextStateAndValue<_S_, _T_> } fromStateToNextStateAndValue - callback function to generate the next state and value + * @param { _S_ } initialState + * @return { SequenceType<_T_> } + */ +const unfold = (initialState, fromStateToNextStateAndValue) => { + + const iterator = () => { + let runningState = initialState; + + const next = () => { + const result = fromStateToNextStateAndValue(runningState); + if (result === undefined) { + return { done: true, value: undefined }; + } else { + runningState = result.state; + return { done: false, value: result.value }; + } + }; + return { next }; + }; + + return createMonadicSequence(iterator); +}; diff --git a/docs/src/kolibri/sequence/constructors/unfold/unfoldTest.js b/docs/src/kolibri/sequence/constructors/unfold/unfoldTest.js new file mode 100644 index 00000000..3e196a3e --- /dev/null +++ b/docs/src/kolibri/sequence/constructors/unfold/unfoldTest.js @@ -0,0 +1,25 @@ +import { TestSuite } from "../../../util/test.js"; +import { unfold } from "./unfold.js"; +import { Seq } from "../seq/seq.js"; +import { Range } from "../range/range.js"; +import { nil } from "../nil/nil.js"; + +const testSuite = TestSuite("Sequence: constructor unfold"); + +testSuite.add("common usage", assert => { + + const empty = unfold(undefined, _ => undefined); + assert.iterableEq(empty, nil); + + const zeroToFour = unfold(0, n => n < 5 ? {state: n + 1, value: n} : undefined); + assert.iterableEq(zeroToFour, Range(4).take(5)); + + const infiniteFibs = unfold( + {last: 0, current: 1}, + ( {last, current}) => ({ state: {last: current, current: last + current}, value: last }) + ); + assert.iterableEq(infiniteFibs.take(10), Seq(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)); + +}); + +testSuite.run();