]> Lady’s Gitweb - Pisces/blobdiff - iterable.js
Add iterator function builders; use in string.js
[Pisces] / iterable.js
diff --git a/iterable.js b/iterable.js
new file mode 100644 (file)
index 0000000..d89e35f
--- /dev/null
@@ -0,0 +1,394 @@
+// ♓🌟 Piscēs ∷ iterable.js
+// ====================================================================
+//
+// Copyright © 2023 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+import { bind, call, identity } from "./function.js";
+import {
+  defineOwnProperty,
+  getPrototype,
+  objectCreate,
+} from "./object.js";
+import { ITERATOR, TO_STRING_TAG } from "./value.js";
+
+export const {
+  /**
+   * Returns an iterator function which iterates over the values in its
+   * provided arraylike object and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with an array, returns an iterator.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  arrayIteratorFunction,
+
+  /**
+   * Returns an iterator function which iterates over the values
+   * yielded by its provided generator function and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with a generator function, returns an
+   * iterator.
+   *
+   * ☡ There is no way to detect whether the function provided to the
+   * returned function is, in fact, a generator function. Consequently,
+   * if a non‐generator function is provided, it will not throw until
+   * the first attempt at reading a value.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  generatorIteratorFunction,
+
+  /**
+   * Returns an iterator function which iterates over the values in its
+   * provided map and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with a map, returns an iterator.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  mapIteratorFunction,
+
+  /**
+   * Returns an iterator function which iterates over the values in its
+   * provided set and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with a set, returns an iterator.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  setIteratorFunction,
+
+  /**
+   * Returns an iterator function which iterates over the characters in
+   * its provided string and yields them.
+   *
+   * If a first argument is provided, it must be a generator function.
+   * The yielded values are instead those yielded by calling that
+   * function with each value in turn.
+   *
+   * If a second argument is provided, it is used as the string tag for
+   * the resulting iterator.
+   *
+   * The resulting function also takes a second argument, which will be
+   * used as the `this` value when calling the provided generator
+   * function, if provided.
+   *
+   * ※ This iterator function iterates over characters; use
+   * `arrayIteratorFunction` to iterate over code units.
+   *
+   * ※ The returned function is an ordinary nonconstructible (arrow)
+   * function which, when called with a string, returns an iterator.
+   *
+   * ※ The prototypes of iterators returned by a single iterator
+   * function will all be the same.
+   */
+  stringIteratorFunction,
+} = (() => {
+  const { [ITERATOR]: arrayIterator } = Array.prototype;
+  const arrayIteratorPrototype = getPrototype(
+    [][ITERATOR](),
+  );
+  const { next: arrayIteratorNext } = arrayIteratorPrototype;
+  const { [ITERATOR]: stringIterator } = String.prototype;
+  const stringIteratorPrototype = getPrototype(
+    ""[ITERATOR](),
+  );
+  const { next: stringIteratorNext } = stringIteratorPrototype;
+  const { [ITERATOR]: mapIterator } = Map.prototype;
+  const mapIteratorPrototype = getPrototype(
+    new Map()[ITERATOR](),
+  );
+  const { next: mapIteratorNext } = mapIteratorPrototype;
+  const { [ITERATOR]: setIterator } = Set.prototype;
+  const setIteratorPrototype = getPrototype(
+    new Set()[ITERATOR](),
+  );
+  const { next: setIteratorNext } = setIteratorPrototype;
+  const generatorIteratorPrototype = getPrototype(
+    function* () {}.prototype,
+  );
+  const { next: generatorIteratorNext } = generatorIteratorPrototype;
+  const iteratorPrototype = getPrototype(
+    generatorIteratorPrototype,
+  );
+
+  /**
+   * An iterator generated by an iterator function.
+   *
+   * This class provides the internal data structure of all the
+   * iterator functions as well as the `::next` behaviour they all use.
+   *
+   * ※ This class extends the identity function to allow for arbitrary
+   * construction of its superclass instance based on the provided
+   * prototype.
+   *
+   * ※ This class is not exposed.
+   */
+  const Iterator = class extends identity {
+    #baseIterator;
+    #baseIteratorNext;
+    #generateNext;
+    #nextIterator = null;
+
+    /**
+     * Constructs a new iterator with the provided prototype which
+     * wraps the provided base iterator (advance·able using the
+     * provided next method) and yields the result of calling the
+     * provided generator function with each value (or just yields each
+     * value if no generator function is provided).
+     *
+     * ☡ It is not possible to type·check the provided next method or
+     * generator function to ensure that they actually are correct and
+     * appropriately callable; if they aren’t, an error will be thrown
+     * when attempting to yield the first value.
+     */
+    constructor(
+      prototype,
+      baseIterator,
+      baseIteratorNext,
+      generateNext = null,
+    ) {
+      super(objectCreate(prototype));
+      this.#baseIterator = baseIterator;
+      this.#baseIteratorNext = baseIteratorNext;
+      this.#generateNext = generateNext;
+    }
+
+    /**
+     * Returns an object conforming to the requirements of the iterator
+     * protocol, yielding successive iterator values.
+     */
+    next() {
+      const baseIteratorNext = this.#baseIteratorNext;
+      const generateNext = this.#generateNext;
+      while (true) {
+        // This function sometimes needs to repeat its processing steps
+        // in the case that a generator function was provided.
+        //
+        // To avoid potentially large amounts of recursive calls, it is
+        // defined in a loop which will exit the first time a suitable
+        // response is acquired.
+        const baseIterator = this.#baseIterator;
+        const nextIterator = this.#nextIterator;
+        if (baseIterator === null) {
+          // The base iterator has already been exhausted.
+          return { value: undefined, done: true };
+        } else if (nextIterator === null) {
+          // This iterator is not currently yielding values from the
+          // provided generator function, either because it doesn’t
+          // exist or because its values have been exhausted.
+          //
+          // Get the next value in the base iterator and either provide
+          // it to the generator function or yield it, as appropriate.
+          const {
+            value,
+            done,
+          } = call(baseIteratorNext, baseIterator, []);
+          if (done) {
+            // The base iterator is exhausted.
+            //
+            // Free it up from memory and rerun these steps to signal
+            // that the iteration has completed.
+            this.#baseIterator = null;
+            this.#generateNext = null;
+            continue;
+          } else if (generateNext !== null) {
+            // The base iterator has yielded another value and there is
+            // a generator function.
+            //
+            // Initialize a new iterator of result values by calling
+            // the function with the current value, and then rerun
+            // these steps to actually yield a value from it.
+            this.#nextIterator = generateNext(value);
+            continue;
+          } else {
+            // The base iterator has yielded another value and there is
+            // no generator function.
+            //
+            // Just yield the value.
+            return { value, done: false };
+          }
+        } else {
+          // This iterator is currently yielding values from the
+          // provided generator function.
+          const {
+            value,
+            done,
+          } = call(generatorIteratorNext, nextIterator, []);
+          if (done) {
+            // The current iterator of values from the provided
+            // generator function is exhausted.
+            //
+            // Remove it, and rerun these steps to advance to the next
+            // value in the base iterator.
+            this.#nextIterator = null;
+            continue;
+          } else {
+            // The current iterator of values has yielded another
+            // value; reyield it.
+            return { value, done: false };
+          }
+        }
+      }
+    }
+  };
+
+  const {
+    next: iteratorNext,
+  } = Iterator.prototype;
+  const makePrototype = (stringTag) =>
+    objectCreate(
+      iteratorPrototype,
+      {
+        next: {
+          configurable: true,
+          enumerable: false,
+          value: function next() {
+            return call(iteratorNext, this, []);
+          },
+          writable: true,
+        },
+        [TO_STRING_TAG]: {
+          configurable: true,
+          enumerable: false,
+          value: stringTag,
+          writable: false,
+        },
+      },
+    );
+
+  /**
+   * Returns a new function capable of generating iterators using the
+   * provided iterator generation method, iterator advancement method,
+   * optional generator function, and optional string tag.
+   *
+   * ※ The first two arguments to this function are bound to generate
+   * the actual exported iterator function makers.
+   *
+   * ※ This function is not exposed.
+   */
+  const iteratorFunction = (
+    makeBaseIterator,
+    baseIteratorNext,
+    generateNext = null,
+    stringTag = "Iterator",
+  ) => {
+    const prototype = makePrototype(stringTag); // intentionally cached
+    return ($, thisArg = undefined) =>
+      new Iterator(
+        prototype,
+        call(makeBaseIterator, $, []),
+        baseIteratorNext,
+        generateNext === null ? null : bind(generateNext, thisArg, []),
+      );
+  };
+
+  return {
+    arrayIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [arrayIterator, arrayIteratorNext],
+      ),
+      "name",
+      { value: "arrayIteratorFunction" },
+    ),
+    generatorIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [
+          function () {
+            return this();
+          },
+          generatorIteratorNext,
+        ],
+      ),
+      "name",
+      { value: "generatorIteratorFunction" },
+    ),
+    mapIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [mapIterator, mapIteratorNext],
+      ),
+      "name",
+      { value: "mapIteratorFunction" },
+    ),
+    setIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [setIterator, setIteratorNext],
+      ),
+      "name",
+      { value: "setIteratorFunction" },
+    ),
+    stringIteratorFunction: defineOwnProperty(
+      bind(
+        iteratorFunction,
+        undefined,
+        [stringIterator, stringIteratorNext],
+      ),
+      "name",
+      { value: "stringIteratorFunction" },
+    ),
+  };
+})();
This page took 0.318101 seconds and 4 git commands to generate.