• Stars
    star
    193
  • Rank 201,081 (Top 4 %)
  • Language
    JavaScript
  • License
    ISC License
  • Created over 8 years ago
  • Updated about 1 year ago

Reviews

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

Repository Details

pipes values through functions, an alternative to using the proposed pipe operator ( |> ) for ES

PPIPE

build Coverage Status npm npm license DeepScan Grade

Please note that this library is considered "done". It is still maintained and will be in the foreseeable future, but, other than adding Typescript support, no new functionality will be added. At least, there is no plan to do so. This library has an extensive test suite with 100% coverage, and it is used by at least a few well-established projects in production. The mythical "production-ready" seems to be reached :)

All bug reports and suggestions are still welcome!

pipes values through functions, an alternative to using the proposed pipe operator ( |> ) for ES.

Demo available on RunKit.

Supports functions returning promises too. In that case, the result of the chain will also be a promise. This is similar to the proposed support for await in the chained functions.

Installation

npm install ppipe

Problems ppipe solves

Let's assume you have these functions:

const add = (x, y) => x + y;
const square = x => x * x;
const divide = (x, y) => x / y;
const double = x => x + x;

How do you pass the results from one to another?

//good old single line solution
add(divide(square(double(add(1, 1))), 8), 1);
//try to get creative with variable names?
const incremented = add(1, 1);
const doubled = double(incremented);
//...

An ideal solution would have been having a pipe operator (|>) but we don't have it. Here is where ppipe comes in.

Order of arguments can be manipulated using the _ property of ppipe function. The result of the previous function is inserted to its place if it exists in the arguments. It can also occur more than once if you want to pass the same parameter more than once. If, and only if, _ doesn't exist among the arguments, the piped value will be inserted at the end.

const ppipe = require("ppipe");
const _ = ppipe._;
ppipe(1)(add, 1)(double)(square)(divide, _, 8)(add, 1)(); // 3

If that is too lisp-y, you can also use ".pipe".

ppipe(1)
  .pipe(add, 1)
  .pipe(double)
  .pipe(square)
  .pipe(divide, _, 8)
  .pipe(add, 1)(); // 3

And then you receive some new "requirements", which end up making the "double" function async...

async function asyncDouble(x) {
  const result = x * 2;
  await someAPICall(result);
  return result;
}

Here are the changes you need to make:

await ppipe(1)
  .pipe(add, 1)
  .pipe(asyncDouble)
  .pipe(square)
  .pipe(divide, _, 8)
  .pipe(add, 1); //3 (you can also use .then and .catch)

Yes, ppipe automatically turns the end result into a promise, if one or more functions in the chain return a promise. It also waits for the resolution and passes the unwrapped value to the next function. You can also catch the errors with .catch like a standard promise or use try/catch in an async function. You meet the requirements and keep the code tidy.

For consistency, the .then and .catch methods are always available, so you don't have to care if any function in the chain is async as long as you use those.

So, later you receive some new "requirements", which make our now infamous double function return an object:

async function asyncComplexDouble(x) {
  const result = x * 2;
  const someInfo = await someAPICall(result);
  return { result, someInfo };
}

Still not a problem:

await ppipe(1)
  .pipe(add, 1)
  .pipe(asyncComplexDouble)
  //pipe._ is also a proxy which saves the property accesses to pluck the prop from the
  //previous function's result later
  .pipe(square, _.result)
  .pipe(divide, _, 8)
  .pipe(add, 1); //3

//well, if you think that might not be clear, you can write it like this, too
await ppipe(1)
  .pipe(add, 1)
  .pipe(asyncComplexDouble)
  .pipe(x => x.result)
  .pipe(square)
  .pipe(divide, _, 8)
  .pipe(add, 1); //3

//this also works
await ppipe(1)
  .pipe(add, 1)
  .pipe(asyncComplexDouble)
  //promises will be unboxed and properties will be returned as getter functions
  //the methods will be available in the chain as well, as shown in the next example
  .result()
  .pipe(square)
  .pipe(divide, _, 8)
  .pipe(add, 1); //3

Let's go one step further; what if you need to access a method from the result?

async function advancedDouble(x) {
  const result = x * 2;
  const someInfo = await someAPICall(result);
  return {
    getResult() {
      return result;
    },
    someInfo
  };
}

There you go:

await ppipe(1)
  .pipe(add, 1)
  .pipe(advancedDouble)
  .getResult()
  .pipe(square)
  .pipe(divide, _, 8)
  .pipe(add, 1); //3

Some More Examples

It is possible to expand the iterable result

const addAll = (...x) => x.reduce((a, b) => a + b, 0)
ppipe([1,2,3]).map(x => x + 1).pipe(addAll, ..._)(); //9

It is possible to reach array members:

await ppipe(10)
    .pipe(asyncComplexDoubleArray)
    .pipe((x, y) => x + y, _[1], _[2]);

Also object properties:

ppipe(10)
    .pipe(x => ({multipliers: [10,20], value: x}))
    .pipe((x, y) => x * y, _.multipliers[0], _.value)(); //100

And you can omit the function altogether if you just want to extract values:

ppipe({multipliers: [10,20], value: 10}).pipe(_.value)(); //10
await ppipe({multipliers: [10,20], value: 10}).pipe(_.value); //10

And as you've seen before, you can always omit the ".pipe", as long as you know how to keep ASI in check:

ppipe({multipliers: [10,20], value: 10})(_.value)(); //10
await ppipe({multipliers: [10,20], value: 10})(_.value); //10

Advanced Functionality

Chain Methods / Properties

You can use these from the chain (after creating one with ppipe(val)).

.with(ctx)

Calls the following function in chain with the given this value (ctx). After calling .with the chain can be continued with the methods from the ctx.

class Example {
  constructor(myInt) {
    this.foo = Promise.resolve(myInt);
  }
  addToFoo(x) {
    return this.foo.then(foo => foo + x);
  }
}
await ppipe(10).with(new Example(5)).addToFoo(_); //15

Look at the test/test.js for more examples.

.val

Gets the current value from the chain. Will be a promise if any function in the chain returns a promise. Calling the chain with no parameters achieves the same result.

Extending Ppipe

You can create an extended instance of ppipe via .extend.

const newPipe = ppipe.extend({
  divide (x, y) {
    return x / y;
  },
  log(...params) {
    console.log(...params);
    return params[params.length - 1];
  }
});
const res = await newPipe(10)
  .pipe(x => x + 1)
  .divide(_, 11)
  .log("here is our x: ") //logs "here is our x: 1"
  .pipe(x => x + 1) // 2

You can also call .extend on the extended ppipes. It will create a new ppipe with the new and existing extensions merged.

Testing

All the functionality is tested, with 100% coverage. This is also integrated in the build process.

To run the tests yourself, clone the repository, install the dev dependencies, and run the npm test command.

npm install

npm test

Contributing

See CONTRIBUTING.

Changelog

  • v2.5.0 - placeholder can be the only argument to the .pipe, for just extracting a property or path
  • v2.4.0 - allow deep property extraction via the placeholder (_.deeply.nested.prop) (test: should be able to extract array members)
  • v2.3.0 - now supports expanding the placeholder (..._) (test: should support expanding the array result)

Caveats

  • This library was not written with performance in mind. So, it makes next to no sense to use it in, say, a tight loop. Use in a web-server should be fine as long as you don't have tight response-time requirements. General rule of thumb: Test it before putting it into prod. There are a lot of tests written for ppipe but none of them measure performance. I may improve the performance in the future (some low-hanging fruits) but I'd rather avoid making any guarantees. Well, there is one good news: Chrome team is working on performance improvements to the Proxy which will very positively affect ppipe performance.

  • It uses ES6 Proxies to do its magic. Proxies are not back-portable. 1.x.x versions of ppipe didn't use proxies. So you can try using an older version with a transpiler if evergreen sounds alien to you. Here is an older stable version without value extracting and context change support.

  • ppipe is not typed. No type definition exists for TypeScript nor Flow. I actually love TypeScript and would support it but the lack of variadic generic type parameters make it next to impossible to provide type definitions for ppipe. More can be read here. Also, ppipe is as dynamic as it gets, giving the ability to access virtual properties/methods which may belong to the provided context, the processed value or any of the possible extensions. TypeScripts Type System is Turing Complete, so, maybe there is a way to type all of this but I really need help about that.