npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

pips

v1.0.0

Published

Type-safe, Magical JavaScript Pipes

Downloads

678

Readme

const farenheit = 50;
const celsius = pipe(farenheit)
    (f => f - 32)
    (f => (f * 5) / 9)
  ();

console.log(celsius); // 10
// Code is presented forwards, not backwards, making it clearer

const obj = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const noOdds = pipe(obj)
    (it => Object.entries(it))
    (it => it.filter(([k, v]) => v % 2 === 0))
    (it => Object.fromEntries(it))
  ();

console.log(noOdds); // { b: 2, d: 4 };
// Make it even better with FP utility libs such as rhax, lodash or ramda

import { entries, filter, toObject } from "rhax";

const obj2 = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const noOdds2 = pipe(obj)
    (entries)
    (filter.object(([k, v]) => v % 2 === 0))
    (toObject)
  ();

console.log(noOdds2); // { b: 2, d: 4 };

Features

  • 🧠 Write code the way humans think - forwards, not backwards.
  • ☀️ Write declarative, elegant, clear code.
  • 🎨 Replace clumsy code blocks with expressive expressions.
  • 🌱 Virtually no footprint, in size and runtime.

API

Pips exposes a single function pipe that creates a Pipe, which you can think of as some sort of box. You can give this box a value (e.g. pipe(x)) and it'll hold it, or create an "empty" box with pipe().

This box can then be given functions one after another, each of which transform what's inside it.

Finally, when our computation is complete, we can get the value inside the box back by calling it with no arguments - ().

Complete signatures can be found in the library's very short source code, if you're into that sort of thing.

The why

Let's demonstrate the motivation for using pips with an example.

You have an object (Record) with Todo objects as values and their ids (unique strings) as keys. You want to filter it and get a new record with the same structure, but whose entries contain only the ongoing todos (indicated by a completed field).

Let's tackle the problem in its general form: You want to implement a variant of Array.filter for objects - given an object, you want to return another object containing a subset of the original's entries, based on some condition (a function that takes a value, and returns true if it's to be kept or false otherwise).

There are many ways to implement this. We'll compare a couple:

Using good ol' for loops and imperative style:

function filterObject<T>(record: Record<string, T>, condition: (v: T) => boolean) {
  const filtered = {};
  for(const [k, v] of Object.entries(record)) {
    if(confition(v)) {
      filtered[k] = v;
    }
  }

  return filtered;
}

It's a good start, but imperative code is more prone to (developer) errors, and it contains a lot of "gray syntax" mixed with actual logic.

Next, using modern syntax, with a preference for array methods over loops:

const filterObject = <T>(record: Record<string, T>, condition: (v: T) => boolean) =>
  Object.fromEntries(
    Object.entries(record).filter(([k, v]) => condition(v))
  );

This one is shorter, somewhat more declarative, and less prone to (developer) errors, but the logic is still all over the place! The code is structured as such - return the object created from the entries gained by record's entries by filtering key-value pairs based on condition.

It's correct, but you likely took a moment to wrap your head around what that sentence exactly means; and it's not because of you, but rather the code's messy order of operations!

In abstract, we humans deal much better with understanding a process linearily, in the way that it actually unfolds. In simple terms, we think forwards, not backwards. This is the reason people have a hard time with recursion, and, more generally, this is why function composition (in the sciences, especially math) is tricky - We see f(g(h(x))), and we think f is first, then g, then h, but in reality it's the other way around - we take x and throw it into h, then throw the result to g, then into f.

Let's rephrase, then, the above computation in a way that makes sense to humans: Given a record and a condition,

  1. Get the entries of record
  2. Filter them based on the condition
  3. Turn it back to an object

Now it seems simple, and a lot clearer! Note that this is a concrete example of our point about composition - if:

  • f is "turn object to entries"
  • g is "filter entries based on condition"
  • h is "turn entries to object"

Then "filter an object x based on condition" is h(g(f(x))) - but that's an awkward and unclear way to present it.

Using a pipe, we can implement the solution "forwards", just as we would reason about it:

const filterObject = <T>(record: Record<string, T>, condition: (v: T) => boolean) =>
  pipe(record)
    (it => Object.entries(it))
    (it => it.filter(([k, v]) => condition(v)))
    (it => Object.fromEntries(it))
  ();

This code already presents a few benefits - it's declarative, elegant and clear; it's an expression, which are generally more convenient than code blocks (compare the ternary operation's conciseness to an if-else block, with brackets and all).

But, most importantly, the code above expresses the computation in the way that you'd reason about it - it lists the steps in the order they play out. This makes it easier to understand, spot bugs in, and maintain in the long run.

As another added bonus, using a pipe saves you the trouble of coming up with awkward semi-descriptive variable names for the steps inside a computation: Think of the pipe as a box with some value inside. You can give the box a function, and it'll apply it to the value, giving you another box with the transformed value inside. Then you can give it another function, and so on.

Referring to the current value as "what's inside the box" (it in the example above) saves you the trouble of coming up with descriptive names for each step - instead of entries, filteredEntries and filteredRecord, you have three its, without compromising the code's clarity.

Finally, Using utility methods (or a utility library such as Rhax) we can make the code even better, and also achieve optimal type inference:

import { entries, filter, toObject } from 'rhax';

const filterObject = <T>(record: Record<string, T>, condition: (v: T) => boolean) =>
  pipe(record)
    (entries)
    (filter(([k, v]) => condition(v)))
    (toObject)
  ();

Compare that to the first two examples!

How is it implemented?

magic