Git Product home page Git Product logo

pips's Introduction

Pips

A tiny library for declarative, type-safe, elegant pipes.

npm size typescript license


  npm install pips / yarn add pips / pnpm add pips
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 โœจ

pips's People

Contributors

nitzanhen avatar

Stargazers

Lior Vainer avatar  avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.