FxTS Documentation

repository·main·Indexed 22 days ago

https://github.com/marpple/fxts

A functional programming library for TypeScript and JavaScript (@fxts/core) providing lazy evaluation and efficient handling of concurrent asynchronous requests. It features two primary composition patterns: `pipe` for functional composition and `fx` for method chaining. The library includes a comprehensive suite of utilities for composition, predicates, object manipulation, collection processing, and flow control, as well as specialized tools like `concurrent` and `toAsync` for managing parallel asynchronous operations.

Tokens
11.6K
Snippets
45
Records
53
Agent score
78%

What's inside FxTS

  1. Understand error behavior in concurrent execution

    main

    When using the concurrent operator, FxTS behaves similarly to Promise.all. If an error occurs in one of the concurrent tasks, other tasks that have already been initiated will continue to be evaluated. The error is caught by the try-catch block once the current batch of concurrent requests is processed or the error is propagated.

    import { concurrent, filter, map, pipe, toArray, toAsync } from "@fxts/core";
    
    const fetchAsyncError = (a) => {
      if (a === 3) {
        return Promise.reject(`err ${a}`);
      }
      return a;
    };
    
    try {
      await pipe(
        [
          Promise.resolve(1),
          Promise.resolve(2),
          Promise.resolve(3), // When this item is evaluated, `map` function throws an error.
          Promise.resolve(4), // This item is also evaluated.
          Promise.resolve(5), // Is is not evaluated from this item.
          Promise.resolve(6),
        ],
        toAsync,
        map(fetchAsyncError),
        filter((a) => a % 2 === 0),
        concurrent(2), // request 2
        toArray,
      );
    } catch (err) {
      // handle err
    }
  2. Understanding `concurrent` placement in pipelines

    main

    The position of concurrent in a pipeline determines which stage of the process is parallelized.

    1. Applying to the data stream: If concurrent is placed after a map or filter that produces asynchronous values, it controls the concurrency of those specific asynchronous operations.
    2. Behavior with Iterable: concurrent always applies to the Iterable before the length is changed by subsequent operators like take.

    If you need to evaluate an asynchronous predicate within a filter concurrently, you must ensure the stream is prepared correctly. For example, if you want to sequentially map values but then run the filter predicate concurrently, you might need to collect values into an array first or place concurrent specifically after the asynchronous filter stage.

    // Example: Evaluating an asynchronous predicate in filter concurrently
    await pipe(
      range(Infinity),
      toAsync,
      map(fetchApi),
      toArray,
      filter((a) => delay(100, a % 2 === 0)),
      take(3),
      concurrent(3),
      each(console.log),
    );
  3. Core Concepts of FxTS

    main

    FxTS is a functional programming library designed around the following principles:

    • Lazy Evaluation: Allows representing large or potentially infinite enumerable data structures without immediate computation.
    • Concurrency Control: Provides tools to handle multiple asynchronous requests while controlling the request count.
    • Type Inference: Leverages TypeScript to automatically infer types through function composition.
    • Iteration Protocols: Strictly follows the standard JavaScript Iterable and AsyncIterable protocols, ensuring compatibility with native language features.
  4. Understand Lazy Evaluation in FxTS

    main

    FxTS supports lazy evaluation, allowing you to process large or infinite data sets efficiently. Unlike standard array methods (like Array.prototype.filter or map) which create new intermediate arrays and traverse the entire collection at every step, FxTS uses Iterable and AsyncIterable to evaluate values only as needed.

    When using pipe with lazy functions, operations like take(n) will stop the evaluation of the upstream pipeline as soon as the requirement is met. This prevents unnecessary computations and traversals.

    Key distinctions:

    • Lazy functions: Do not evaluate actual values immediately. They return an iterable structure that represents the computation pipeline (similar to a generator).
    • Strict functions: These are terminal operations that trigger the actual evaluation of the lazy pipeline (e.g., toArray, reduce, head).
    // Standard array approach (Eager/Not Lazy)
    // Creates intermediate arrays at every step
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      .filter((a) => a % 2 === 0)
      .map((a) => a * a)
      .reduce((a, b) => a + b);
    
    // FxTS approach (Lazy)
    // Only evaluates what is necessary to satisfy 'take(2)'
    pipe(
      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
      filter((a) => a % 2 === 0),
      map((a) => a * a),
      take(2),
      reduce((a, b) => a + b),
    );
  5. Use method chaining with fx

    main

    The fx wrapper allows for fluent, readable functional programming by providing data transformation methods directly on the returned object. You can chain methods like .filter(), .map(), .take(), and .reduce() to process Iterable or AsyncIterable values.

    Important: Lazy Evaluation fx defaults to lazy evaluation. Transformations are not actually executed until you call a strict evaluation method such as .toArray(), .groupBy(), .indexBy(), .some(), or .reduce().

    fx([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
      .filter((a) => a % 2 === 0) // [0, 2, 4, 6, 8]
      .map((a) => a * a) // [0, 4, 16, 36, 64]
      .take(2) // [0, 4]
      .reduce(sum); // 4
    
    fx("abc")
      .map((a) => a.toUpperCase()) // ["A", "B", "C"]
      .take(2)
      .toArray(); // ["A", "B"]
  6. Debug lazy-evaluated pipelines with peek

    main

    In lazy-evaluated pipelines, functions return an IterableIterator rather than the actual data. Because nothing is processed until a terminal function like toArray is called, using tap will only show you the IterableIterator object itself, which is not helpful for inspecting the data flowing through the pipeline.

    To inspect the actual values being yielded during lazy evaluation, use the peek function instead of tap. peek allows you to observe each item as it is lazily produced by the iterator.

    // Using tap in lazy pipelines only shows the iterator object:
    pipe(
      range(1, Infinity),
      map(addDateFrom(new Date(2000, 0, 1))),
      filter(is13thOfFriday),
      tap(console.log), // Logs: IterableIterator
      map(formatYYYYMMDD),
      tap(console.log), // Logs: IterableIterator
      take(5),
      toArray,
    );
    
    // Use peek to see the actual values during lazy evaluation:
    pipe(
      range(1, Infinity),
      map(addDateFrom(new Date(2000, 0, 1))),
      filter(is13thOfFriday),
      peek(console.log), // Logs actual values as they are yielded
      map(formatYYYYMMDD),
      peek(console.log), // Logs actual values as they are yielded
      take(5),
      toArray,
    );
  7. When to use the `toAsync` function

    main

    While many FxTS functions support both Iterable and AsyncIterable, you cannot iterate over a standard Iterable if your callback function is asynchronous or if you are dealing with an Iterable<Promise<T>>.

    To handle asynchronous logic or promises within an iteration, you must first convert the source to an AsyncIterable using the toAsync function. This ensures that the FxTS pipeline correctly awaits the values and the callback results.

    // If your callback is async or your iterable contains promises,
    // use toAsync to convert it to an AsyncIterable.
    
    await pipe(
      numbers(), // Iterable<number>
      toAsync,   // AsyncIterable<number>
      find((num) => Promise.resolve(num === 2)),
    );
    
    await pipe(
      promiseNumbers(), // Iterable<Promise<number>>
      toAsync,          // AsyncIterable<number>
      find((num) => Promise.resolve(num === 2)),
    );
  8. Handle concurrent async operations with concurrent and toAsync

    main

    You can manage parallel asynchronous operations by converting iterables to async iterables using toAsync and controlling the execution flow with concurrent. The concurrent(concurrency) function allows you to specify the number of parallel operations to run at once.

    import { concurrent, countBy, flat, fx, map, pipe, toAsync } from "@fxts/core";
    
    // Example: Fetching multiple pages with controlled concurrency
    const fetchWiki = (page: string) =>
      fetch(`https://en.wikipedia.org/w/api.php?action=parse&page=${page}`);
    
    const countWords = async (concurrency: number) =>
      pipe(
        ["html", "css", "javascript", "typescript"],
        toAsync,
        map(fetchWiki),
        map((res) => res.text()),
        map((words) => words.split(" ")),
        flat,
        concurrent(concurrency),
        countBy((word) => word),
      );
    
    await countWords(2); // Executes with a concurrency limit of 2
  9. Handle synchronous errors in FxTS pipes

    main

    FxTS follows standard JavaScript error propagation protocols. For synchronous operations within a pipe, you can use standard try-catch blocks to capture errors thrown by any function in the chain.

    import { map, pipe, take, toArray, toAsync } from "@fxts/core";
    
    const syncError = (a) => {
      throw new Error(`err ${a}`);
    };
    
    try {
      pipe(
        [1, 2, 3, 4, 5],
        map(syncError),
        filter((a) => a % 2 === 0),
        toArray,
      );
    } catch (err) {
      // handle err
    }