• Stars
    star
    131
  • Rank 275,867 (Top 6 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 7 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Ergonomic JavaScript/TypeScript transducers for beginners and experts.

Transducist

Ergonomic JavaScript/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(haskellProjects)
    .map(project => project.owner)
    .filter(owner => owner.name === "Brad")
    .flatMap(owner => owner.children)
    .take(100)
    .toArray();

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 implemented with transducers and exposes all benefits of the transducer protocol, such as being able to easily add novel 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-circuiting. 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.

  • …fast! Transducist performs efficient computations by never creating more objects than necessary. See the benchmarks for details.

  • …typesafe. Transducist is written in TypeScript and is designed to be fully typesafe without requiring you to manually specify type parameters everywhere.

  • …small. Transducist is less than 4kB gzipped, and can be made even smaller through tree shaking.

Installation

With Yarn:

yarn add transducist

With NPM:

npm install transducist

This library, with the exception of the functions which relate to Set and Map, works fine on ES5 without any polyfills or transpilation, but its TypeScript definitions depend on ES6 definitions for the Iterable type. If you use TypeScript in your project, you must make definitions for these types 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.

Furthermore, the methods toSet, toMap, and toMapGroupBy assume the presence of ES6 Set and Map classes in your environment. If you wish to use these methods, you must ensure your environment has these classes or provide a polyfill.

Basic Usage

Import with

import { chainFrom } from "transducist";

Start a chain by calling chainFrom() on any iterable, such as an array, a string, or an ES6 Set.

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(), .find(), and .toSet(), among many others. For a particularly interesting one, see .toMapGroupBy().

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

Iterable utilities

Transducist also comes with a handful of iterable helpers for common sequences, which are often useful as the starter for a chain. For example:

chainFrom(range(5))
    .map(i => i * i)
    .toArray(); // -> [0, 1, 4, 9, 16]

chainFrom(repeat("x", 5)).joinToString(""); // -> "xxxxx"

All such iterables generate values only when needed, which means they can represent even infinite sequences:

chainFrom(range(0, Number.POSITIVE_INFINITY))
    .map(n => n * n)
    .takeWhile(n => n < 20)
    .toArray(); // -> [0, 1, 4, 9, 16]

For a full list of iterables, see the Iterables 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 objects that satisfy the transducer protocol can be added to the chain using the .compose() method, allowing you to write new types of transforms that can be included in the middle of the chain without losing the benefits of early termination and no intermediate array creation. This includes transducers defined by other libraries, so we could for instance reuse a transducer from transducers.js as follows:

import { chainFrom } from "transducist";
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];

All of this library'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 "transducist";

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.

Bundle Size and Tree Shaking

If you are using a bundler which supports tree shaking (e.g. Webpack 4+, Rollup) and are looking to decrease bundle size, Transducist also provides an alternate API to allow you to only pay for the functions you actually use, which incidentally is similar to the API provided by more typical transducer libraries. All chain methods are also available as standalone functions and can be used as follows:

import { compose, filter, map, toArray, transduce } from "transducist";

transduce(
    [1, 2, 3, 4, 5],
    compose(
        filter(x => x > 2),
        map(x => 2 * x),
    ),
    toArray(),
); // -> [6, 8, 10]

which is equivalent to the fluent version:

import { chainFrom } from "transducist";

chainFrom([1, 2, 3, 4, 5])
    .filter(x => x > 2)
    .map(x => 2 * x)
    .toArray(); // -> [6, 8, 10]

However, the standalone function version of this example adds a mere 1.64 kB to bundle size (pre-gzip), compared to the chained version which adds 11.1 kB (as of version 1.0.0). Note that after gzipping, the fluent version is below 4kB as well.

For details, see the tree shaking API section of the API docs.

Benchmarks

View the benchmarks.

API

View the full API docs.

Copyright © 2017 David Philipson

More Repositories

1

typescript-fsa-reducers

Fluent syntax for defining typesafe reducers on top of typescript-fsa.
TypeScript
219
star
2

typescript-string-enums

Typesafe string enums in TypeScript pre-2.4.
TypeScript
81
star
3

sturdy-websocket

Tiny WebSocket wrapper that reconnects and resends failed messages.
TypeScript
19
star
4

untruncate-json

Fix up the end of a partial JSON string to create valid JSON.
TypeScript
18
star
5

resentence

Easy-to-use React component for morphing one string into another.
TypeScript
10
star
6

graphito

View graphs on a small screen
Clojure
5
star
7

meteor-react-typescript-todos

A conversion of the Meteor-React sample app to use TypeScript
TypeScript
4
star
8

graphs-and-paths

Tools for graphs representing 2-D spatial points and links between them.
TypeScript
4
star
9

broken-loop

Helpers for breaking up long-running computations in JavaScript.
TypeScript
3
star
10

graphs-and-paths-demo

Demo page for the graphs-and-paths library
TypeScript
3
star
11

advent-of-code-2021

A framework and solutions for Advent of Code 2021 (adventofcode.com)
Rust
2
star
12

pure-assign

Drop-in replacement for Object.assign() for "updating" immutable objects.
TypeScript
2
star
13

simple-geomath

Minimalist library for math involving latitude and longitude.
TypeScript
2
star
14

advent-of-code-2019

Solutions for Advent of Code 2019 (https://adventofcode.com/2019)
Kotlin
2
star
15

rangle

A loop drawing game that rewards quick thinking
Clojure
2
star
16

gaia-project-timer

A phone-based game timer for the board game Gaia Project
Clojure
2
star
17

grasky

A 3-d graph library using WebGL
CoffeeScript
2
star
18

timer-mystica

A phone-based game timer for the board game Terra Mystica
Clojure
2
star
19

hank-ball

Web page displaying Hank on a ball.
TypeScript
1
star
20

sample_app

Ruby on Rails Tutorial sample application
Ruby
1
star
21

word_guesser

A word-based variant on the classic "hi-lo" game written in Haskell.
Haskell
1
star
22

grpc-web-hello

My adventures trying out gRPC-Web with a Rust backend
Rust
1
star
23

transducist-benchmarks

Benchmarks comparing Transducist to other common function chaining libraries.
TypeScript
1
star
24

chronverna

A phone-based game timer for the board game Caverna
Clojure
1
star
25

word_guesser_web

A word-based variant on the classic "hi-lo" game, as a web application based on Yesod.
CSS
1
star
26

java_and_robotics

Website for the EPGY Java and Robotics course, 2012
Ruby
1
star