0.1.1 • Published 7 years ago

typescript-transducers v0.1.1

Weekly downloads
2
License
MIT
Repository
github
Last release
7 years ago

TypeScript Transducers

Ergonomic TypeScript transducers for beginners and experts.

Build
Status

Table of Contents

Introduction

This library will let you write code that looks like this:

// Let's find 100 people who have a parent named Brad who runs Haskell projects
// so we can ask them about their dads Brads' monads.
const result = chainFrom(allProjects)
    .filter(project => project.language === "Haskell")
    .map(project => project.owner)
    .filter(owner => owner.name === "Brad")
    .flatMap(owner => owner.children)
    .take(100)
    .forEach(person => console.log(person));

This computation is very efficient because no intermediate arrays are created and work stops early once 100 people are found.

You might be thinking that this looks very similar to chains in Lodash or various other libraries that offer a similar API. But this library is different because it's built on top of transducers-js and exposes all the benefits of using transducers, such as being able to easily add new transformation types to the middle of a chain and producing logic applicable to any data structure, not just arrays.

Never heard of a transducer? Check the links in the transducers-js readme for an introduction to the concept, but note that you don't need to understand anything about transducers to use this library.

Goals

Provide an API for using transducers that is…

  • …easy to use even without transducer knowledge or experience. If you haven't yet wrapped your head around transducers or need to share a codebase with others who haven't, the basic chaining API is fully usable without ever seeing a reference to transducers or anything more advanced than map and filter. However, it is also…

  • …able to reap the full benefits of transducers for those who are familiar with them. By using the general purpose .compose() to place custom transducers in the middle of a chain, any kind of novel transform can be added while still maintaining the efficiency bonuses of laziness and short-cicuiting. Further, the library can also be used to construct standalone transducers which may be used elsewhere by other libraries that incorporate transducers into their API.

  • …convenient with TypeScript IDEs. Typical transducer libraries, such as transducers.js and transducers-js, are hard to use with TypeScript. They depend on calling compose to glue transducers together, which if typed correctly has an ugly type signature with many type parameters and overloads, and which generates cryptic TypeScript errors if something is amiss. Instead, we use a familiar chaining API which grants easy autocompletion in an IDE, as well as aiding readability.

    Of course, this library can be consumed without TypeScript as well. You will lose the typechecking and autocomplete benefits, but keep all the other advantages.

  • …typesafe. Avoid the type fuzziness that is present in other transform chaining APIs. For example, under Lodash's type definitions, the following typechecks:

    const badSum = _([{a: true}, {b: false}]).sum();
    // Returns "[object Object][object Object]", if you're curious.

    and given Lodash's API, there is no way to correctly type this. By contrast, this library has the typesafe

    const goodSum = chainFrom([1, 2, 3]).reduce(toSum()); // -> 6
  • …fast! Typescript-transducers is a thin wrapper on top of transducers-js and is therefore very efficient. See this blog post by the author of transducers.js for some benchmarks. That post is also a great description of some other advantages of transducers.

Installation

With Yarn:

yarn add typescript-transducers

With NPM:

npm install --save typescript-transducers

This library works fine on ES5 without any polyfills or transpilation, but its TypeScript definitions depend on ES6 definitions for the Iterable type. If you use it with TypeScript, you must make definitions for Iterable and Iterator available by doing one of the following:

  • In tsconfig.json, set "target" to "es6" or higher.
  • In tsconfig.json, set "libs" to include "es2015.iterable" or something that includes it.
  • Add the definitions by some other means, such as importing types for es6-shim.

Basic Usage

Import with

import { chainFrom } from "typescript-transducers";

Start a chain by calling chainFrom() on any iterable, including an array or a string (or an object, see the full API).

const result = chainFrom(["a", "bb", "ccc", "dddd", "eeeee"])

Then follow up with any number of transforms.

    .map(s => s.toUpperCase())
    .filter(s => s.length % 2 === 1)
    .take(2)

To finish the chain and get a result out, call a method which terminates the chain and produces a result.

    .toArray(); // -> ["A", "CCC"]

Other terminating methods include .forEach(), .count(), and .find(), among others.

For a list of all possible transformations and terminations, see the full API docs.

Advanced Usage

These advanced usage patterns make use of transducers. If you aren't familiar with transducers yet, see the links in the transducers-js readme for an introduction.

Using custom transducers

Arbitrary transducers that satisfy the transducer protocol can be added to the chain using the .compose() method. This includes transducers defined by other libraries, so we could for instance do

import { chainFrom } from "typescript-transducers";
import { cat } from "transducers.js";

const result = chainFrom([[1, 2], [3, 4, 5], [6]])
    .drop(1)
    .compose(cat)
    .map(x => 10 * x)
    .toArray(); // -> [30, 40, 50, 60];

As an example of implementing a custom transducer, suppose we want to implement a "replace" operation, in which we provide two values and all instances of the first value are replaced by the second one. We can do so as follows:

import {
    CompletingTransformer,
    Transducer,
    Transformer,
 } from "typescript-transducers";

function replace<T>(initial: T, replacement: T): Transducer<T, T> {
    return (xf: CompletingTransformer<T, any, T>) => ({
        ["@@transducer/init"]: () => xf["@@transducer/init"](),
        ["@@transducer/result"]: (result: T) => xf["@@transducer/result"](result),
        ["@@transducer/step"]: (result: T, input: T) => {
            const output = input === initial ? replacement : input;
            return xf["@@transducer/step"](result, output);
        },
    });
}

We could then use it as

const result = chainFrom([1, 2, 3, 4, 5])
    .compose(replace(3, 1000))
    .toArray(); // -> [1, 2, 1000, 4, 5]

If you find yourself doing this a lot, you may want to check out the utility function makeTransducer() to reduce boilerplate, which would allow the above to be written as

function replace<T>(initial: T, replacement: T) {
    return makeTransducer((reducer, result, input) => {
        const output = intput === initial ? replacement : input;
        return reducer(result, output);
    });
}

All of this libary's transformation methods are implemented internally with calls to .compose().

Using custom reductions

Similarly, arbitrary terminating operations can be introduced using the .reduce() method, which can accept not only a plain reducer function (that is, a function of the form (acc, x) => acc) but also any object satisfying the transformer protocol. All of this library's termination methods are implemented internally with a call to .reduce() (with the single exception of .toIterator()).

Creating a standalone transducer

It is also possible to use a chaining API to define a transducer without using it in a computation, so it can be passed around and consumed by other APIs which understand the transducer protocol, such as transduce-stream. This is done by starting the chain by calling transducerBuilder() and calling .build() when done, for example:

import { chainFrom, transducerBuilder } from "typescript-transducers";

const firstThreeOdds = transducerBuilder<number>()
    .filter(n => n % 2 === 1)
    .take(3)
    .build();

Since this returns a transducer, we can also use it ourselves with .compose():

const result = chainFrom([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    .compose(firstThreeOdds)
    .toArray(); // -> [1, 3, 5]

This is a good way to factor out a transformation for reuse.

API

View the full API docs.

Copyright © 2017 David Philipson