TypeScript Transducers
Ergonomic TypeScript transducers for beginners and experts.
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. .filterproject.language === "Haskell" .mapproject.owner .filterowner.name === "Brad" .flatMapowner.children .take100 .forEachconsole.logperson;
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
andfilter
. 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:
;// 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
; // -> 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
;
Start a chain by calling chainFrom()
on any iterable, including an array or a
string (or an object, see the full API).
Then follow up with any number of transforms.
.maps.toUpperCase .filters.length % 2 === 1 .take2
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
;; .drop1 .composecat .map10 * 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:
;
We could then use it as
.composereplace3, 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
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:
; .filtern % 2 === 1 .take3 .build;
Since this returns a transducer, we can also use it ourselves with .compose()
:
.composefirstThreeOdds .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