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

handrail

v2.0.0

Published

a toolset for logical disjunctions / safety for your functional pipelines

Downloads

21

Readme

handrail

a toolset for adding safety to your functional pipelines

Please read the accompanying post for more in depth explanation.

This utility adds logical disjunction / railway-oriented programming to your functional pipelines.

NB: See this file in a runnable form here: example.literate.js

Install

yarn add handrail -S

or

npm i handrail -S

Use

Here's an all-in-one example where we can make an unsafe function safer while not modifying the original:

import {guideRail, fold} from 'handrail'
import pipe from 'ramda/src/pipe'

// here are two potential error cases
const over21 = ({age}) => age > 20
const hasMoney = ({cash}) => cash - 5 >= 0

// and these are the cases we pass to the end, before folding
const growUp = (user) => `Expected ${user.name} to be 21!`
const getAJob = (user) => `Expected ${user.name} to have at least 5 dollars!`

// here's our original function, which has some errors in its assumptions
const bartenderOfIllRepute = (user) => {
  user.cash -= 5
  user.beverages = user.beverages || []
  user.beverages.push(`beer`)
  return user
}

// here's how we fix it with `guideRail`
const bartenderOfGoodRepute = pipe(
  guideRail(
    [
      // add safety for age!
      [over21, growUp],
      // add safety for cash!
      [hasMoney, getAJob]
      // add more!
    ],
    // alter the Either value
    bartenderOfIllRepute
  ),
  // this just pulls our value out from the Either (see the [fold API](https://github.com/brekk/handrail#fold) below)
  fold(I, I)
)

Example

Here's a contrived problem that handrail can help us solve:

  1. Jimmy and Alice want to go drinking, but Jimmy isn't of legal drinking age.

    const resetUsers = () => ({
      alice: {name: `alice`, cash: 15, age: 22},
      jimmy: {name: `jimmy`, cash: 20, age: 20}
    })
    
    let {alice, jimmy} = resetUsers()
    
  2. There's an unscrupulous bartender (in the form of a function) who doesn't enforce the rules.

const unscrupulousBartender = (user) => {
  user.cash -= 5
  user.beverages = user.beverages || []
  user.beverages.push(`beer`)
  return user
}

console.log(`=== example one ===`)
console.log(`alice goes to the bar`, unscrupulousBartender(alice))
// {name: `alice`, cash: 10, beverages: [`beer`], age: 22}
console.log(`jimmy goes to the bar`, unscrupulousBartender(jimmy))
// {name: `jimmy`, cash: 15, beverages: [`beer`], age: 20}
  1. But we're part of a team that's trying to crack down on unscrupulous bartenders, and we'd like to use handrail to solve this problem.
// import {handrail} from "handrail"
const {handrail} = require(`./handrail`)

const ageAttentiveBartender = handrail(
  (user) => user.age > 20,
  (user) => `Expected ${user.name} - (age: ${user.age}) to be at least 21.`,
  unscrupulousBartender
)

console.log(`=== example two ===`)
console.log(`alice goes to the bar behaving legally`, ageAttentiveBartender(alice))
// { r: { name: 'alice', cash: 5, age: 22, beverages: [ 'beer', 'beer' ] } }
console.log(`jimmy goes to the bar behaving legally`, ageAttentiveBartender(jimmy))
// { l: 'Expected jimmy - (age: 20) to be at least 21.' }

Hey, now we're seeing an altered behavior, but why is this {r/l} object wrapped around our values?

This is an Either; it's either a Left or a Right. In either case, when we wanna grab a value out of the result, we simply have to use fold from 'handrail' to get a resolving value.

// import {fold} from 'handrail'
const {fold} = require(`./handrail`)

fold takes three parameters. The first two are functions, the first is invoked when the value is a Left, and the other is invoked when the value is a Right. Finally, the last parameter is an Either (Left / Right). This is a curried function, so you can specify what to do as a resolution well before you have an Either.

// here's a simple one
const logOrWarn = fold(console.error, console.log)

Now we can tack on this resolution value to our previously-error producing function using pipe

const pipe = require(`ramda/src/pipe`)
const ageAttentiveBartender2 = pipe(ageAttentiveBartender, logOrWarn)

console.log(`=== example three ===`)
console.log(`alice goes to the bar behaving legally, round 2`)
ageAttentiveBartender2(alice)
/*
{ name: 'alice',
  cash: 0,
  age: 22,
  beverages: [ 'beer', 'beer', 'beer' ] }
*/
console.log(`jimmy goes to the bar behaving legally, round 2`)
ageAttentiveBartender2(jimmy)

Oh! Now we've added age-safety to our bar!

However, let's say that we've spotted another issue with our current function -- it doesn't care if the given user doesn't have cash to cover the beer.

console.log(`=== example four ===`)
console.log(`alice can go into debt with the bar!`)
ageAttentiveBartender2(alice)
/*
{ name: 'alice',
  cash: -5,
  age: 22,
  beverages: [ 'beer', 'beer', 'beer', 'beer' ] }
*/

So, rather than continuing to make Alice more drunk and more in debt, let's call resetUsers:

let soberUsers = resetUsers()
alice = soberUsers.alice
jimmy = soberUsers.jimmy

And let's see what we can do (relative to our original unscrupulousBartender implementation above) to add both age & cash safety to our function.

We'll use rail and multiRail, which will allow us to add more than one assertion / form of safety to our original bartending function:

// import {rail, multiRail} from 'handrail'
const {rail, multiRail} = require(`./handrail`)

(NB: This example leans a little more heavily on an understanding of pipe, which is described in more detail here. Simple example: pipe((x) => x + 5, (y) => y - 7) is the same as a new function (z) => z - 2)

/* for easier recall:
const unscrupulousBartender = (user) => {
  user.cash -= 5
  user.beverages = user.beverages || []
  user.beverages.push(`beer`)
  return user
}
*/

// we need map so that we can alter things within the Either value
const map = require(`ramda/src/map`)

// let's establish our basic expectations
const usersShouldBe21 = ({age}) => age > 20
const usersShouldHaveCashToCoverABeer = ({cash}) => cash - 5 >= 0

// and the errors we have
const warnYoungsters = (user) => `Expected ${user.name} to be 21!`
const warnWouldBeDebtors = (user) => `Expected ${user.name} to have at least 5 dollars!`

const cashAndAgeSafeBartender = pipe(
  // add safety for age!
  rail(usersShouldBe21, warnYoungsters),
  // add safety for cash!
  // multiRail is identical to rail, but should only be used when rail is already being used
  multiRail(usersShouldHaveCashToCoverABeer, warnWouldBeDebtors),
  // alter the Either value, so wrap our original function in `map`
  map(unscrupulousBartender),
  // convert our Either value to a string and print it
  logOrWarn
)

console.log(`=== example five ===`)
console.log(`jimmy is rejected for being underage:`)
cashAndAgeSafeBartender(jimmy)
// Expected jimmy to be 21!
console.log(`alice buys beer until she is broke:`)
cashAndAgeSafeBartender(alice)
// { name: 'alice', cash: 10, age: 22, beverages: [ 'beer' ] }
cashAndAgeSafeBartender(alice)
// { name: 'alice', cash: 5, age: 22, beverages: [ 'beer', 'beer' ] }
cashAndAgeSafeBartender(alice)
// { name: 'alice', cash: 0, age: 22, beverages: [ 'beer', 'beer', 'beer' ] }
cashAndAgeSafeBartender(alice)
// Expected alice to have at least 5 dollars!

Finally, to round it out, you can use guideRail to automate the above process:

const {guideRail} = require(`./handrail`)

const cashAndAgeSafeBartender2 = guideRail(
  [
    // add safety for age!
    [usersShouldBe21, warnYoungsters],
    // add safety for cash!
    [usersShouldHaveCashToCoverABeer, warnWouldBeDebtors]
    // add more!
  ],
  // alter the Either value
  unscrupulousBartender
)

Changelog

  • 1.0.0 - initial commit
  • 1.0.3 - added null safety
  • 1.0.4 - started using katsu-curry
  • 1.0.5 - added guideRail
  • 1.1.5 - reduced total size
  • 1.2.0 - modularized codebase
  • 1.3.0 - updated dependencies
  • 1.3.3 - fix exports
  • 1.3.4 - swap to jest, update speeds

API

handrail

Parameters

  • assertion function a function to test the input with
  • wrongPath function a function to prepare data before it passes into the Left path
  • rightPath function a function to modify after it passes into the Right path
  • input any any input

Returns (GuidedLeft | GuidedRight) an Either

rail

Add safety to your pipelines!

Parameters

  • assertion function boolean-returning function
  • wrongPath function function invoked if the inputs are bad
  • input any any input

Examples

import {rail} from 'handrail'
import pipe from 'ramda/src/pipe'
const divide = (a, b) => a / b
const safeDivide = curry((a, b) => pipe(
  rail(() => b !== 0, () => `Expected ${b} to not be zero!`),
  divide(a)
)(b)

Returns (GuidedRight | GuidedLeft) Left / Right -wrapped value

multiRail

multiRail is nearly-identical to rail, but should only be used if rail is already in use This is a useful function if you need very granular control of your pipe. If not, you should probably use guideRail instead.

Parameters

  • assertion function boolean-returning function
  • wrongPath function function invoked if the inputs are bad
  • input any any input

Examples

import {rail, multiRail} from 'handrail'
import pipe from 'ramda/src/pipe'
const divide = (a, b) => a / b
const safeDivide = curry((a, b) => pipe(
  rail(() => (typeof a === `number`), () => `Expected ${a} to be a number!`),
  multiRail(() => (typeof b === `number`), () => `Expected ${b} to be a number!`)
  multiRail(() => b !== 0, () => `Expected ${b} to not be zero!`),
  divide(a)
)(b)

Returns (GuidedRight | GuidedLeft) Left / Right -wrapped value

guideRail

Encapsulate error states in a simple structure that returns a Left on error or Right on success

Parameters

  • rails Array<functions> an array of [assertion, failCase] pairs
  • goodPath function what to do if things go well
  • input any whatever

Examples

import pipe from 'ramda/src/pipe'
import {guideRail, fold} from 'handrail'
const identity = (x) => x
const rails = [
  [({age}) => age > 20, ({name}) => `Expected ${name} to be 21.`],
  [({cash}) => cash - 5 >= 0, ({name}) => `Expected ${name} to have cash.`],
]
const bartender = (user) => {
  user.cash -= 5
  user.beverages = user.beverages || []
  user.beverages.push(`beer`)
  return user
}
const cashAndAgeSafeBartender = pipe(
  guideRail(rails, bartender),
  fold(identity, identity)
)

Returns (GuidedLeft | GuidedRight) an Either

bimap

Parameters

  • leftPath function do something if function receives a Left
  • rightPath function do something if function receives a Right
  • either Either either a Left or a Right

Returns Either the original Either, mapped over, but like, with handed-ness

fold

Parameters

  • leftPath function do something if function receives a Left
  • rightPath function do something if function receives a Right
  • either Either either a Left or a Right

Returns any the value from within an Either, pulled out of the monadic box