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

@nozzlegear/railway

v3.2.0

Published

Functional helpers, heavily inspired by F#, to help facilitate "railway oriented" programming.

Downloads

15

Readme

@nozzlegear/railway

This is a collection of functional helpers and monads, heavily inspired by F#, to help facilitate "railway-oriented" programming in TypeScript.

Installation

yarn install @nozzlegear/railway
import { compute, pipe, Result, Async } from "@nozzlegear/railway";

Usage

compute

The compute function is an incredibly simple wrapper whose purpose is to group up temporary variables into a "computation block" (think a let binding in F#), preventing them from polluting your function scope. It also encourages immutability (but does not require it).

const order = await compute(async () => {
    const displayId = await database.getDisplayIdAsync();
    const order = {
        ...baseOrder,
        user_id: req.user_id,
        display_id: displayId
    }
    const result = await database.createAsync(order);

    return {
        ...order,
        _id: result.id,
        _rev: result.rev
    }
})

In this example, order is only declared once, and the temporary variables that only it uses (displayId, result) are wrapped up neatly in the computation. Here's the same example without wrapping in a computation:

const displayId = await database.getDisplayIdAsync();
let order = {
    ...baseOrder,
    user_id: req.user_id,
    display_id: displayId
}
const result = await database.createAsync(order);
order = {
    ...order,
    _id: result.id,
    _rev: result.rev
}

In this example, order is mutable by default (because its value needs to be reassigned after getting the result), and the temporary variables that only it uses are polluting the scope of the rest of the function.

pipe

The pipe function is a simple function chain that pipes the result of the last function to the next function. Since custom operators are impossible, the pipe function instead uses .chain and .value.

Note that the functions are executed as they're chained, not when the value is retrieved.

function getName() {
    return "joshua"
}

function capitalize(str: string) {
    const first = str[0];
    const rest = str.slice(1);

    return first.toUpperCase() + rest.toLowerCase();
}

function formatMessage(name: string) {
    return `Hello, ${name}!`
}

// Pipe the functions together to create the message
const message = pipe(getName())
    .chain(capitalize)
    .chain(formatMessage)
    .value();

console.log(message) // "Hello, Joshua!"

Result<T>

The Result monad encourages railway-oriented programming by wrapping values in either an "Ok" value or an "Error" value. You can safely operate on the monad without worrying about whether the value is an error or not, as the functions will only operate on "Ok" values.

const result = Result.ofValue<string>("5")
    .map(str => parseInt(str));

if (result.isOk()) {
    console.log(result.getValue()) // 5
} else {
    console.error(result.getError())
}

When the value is an error, none of the functions will run:

const result = Result.ofError<string>(new Error("Test error"))
    .map(str => parseInt(str));

if (result.isOk()) {
    console.log(result.getValue()) // will not be called
} else {
    console.error(result.getError()) // Error with message "Test error"
}

Always check that a Result has a value before getting the value, and check that a Result has an error before getting the error. Attemping to do either of these operations without first checking can throw an error and break your program:

const firstResult = Result.ofError(new Error("Test error"));

firstResult.getValue() // This WILL throw an error

const secondResult = Result.ofValue(5)

secondResult.getError() // This WILL throw an error

You can set a default value for the Result, which will be used if the value is an error:

const value = Result.ofError<string>(new Error("Test error"))
    .map(str => parseInt(str))
    .defaultValue(10)

console.log(value) // 10

The Result monad's functions can also be curried, which is ideal for working with the pipe function:

function getName() {
    return "joshua"
}

function capitalize(str: string) {
    const first = str[0];
    const rest = str.slice(1);

    return first.toUpperCase() + rest.toLowerCase();
}

function formatMessage(name: string) {
    return `Hello, ${name}!`
}

// Pipe the functions together to create the message, using the Result monad's currying
const message = pipe(getName())
    .chain(Result.ofValue)
    .chain(Result.map(capitalize))
    .chain(Result.map(formatMessage))
    .chain(Result.defaultValue("Hello, Newman..."))
    .value();

console.log(message) // "Hello, Joshua!"

You can wrap functions and promises in a Result, which will internally wrap them in a try/catch (or add a .catch to the promise chain):

const fnResult = Result.ofFunction(() => {
    return somethingUndefined / 0
})

console.log(fnResult.isError()) // true

if (fnResult.isError()) {
    console.error(fnResult.getError()) // ReferenceError: somethingUndefined is not defined at ....
}

// Be sure to await the result
const promResult = await Result.ofPromise(async () => {
    return 5
})

console.log(promResult.isOk()) // true

if (promResult.isOk()) {
    console.log(promResult.getValue()) // 5
}

Async<T>

The Async monad wraps a promise and adds a couple of functions that make it easier to work with the value of the promise. The biggest change is that Async.map takes the value of the promise and returns the exact value you return, where the native Promise.map would return an array of the value you return.

async function getSomethingAsync() {
    return "5";
}

const value = await Async.ofPromise(getSomethingAsync())
    .map(parseInt)
    .get() // Must call .get() to get the final promise so you can await it

console.log(value) // 5

// Doing the same thing with a promise would return an array:
const secondValue = await getSomethingAsync().map(parseInt)

console.log(secondValue) // [5]

You can also bind promises, merging the promise returned by the function into the value of the Async's promise:

async function getSomethingAsync() {
    return "5"
}

async function parseIntAsync(str: string) {
    return parseInt(str);
}

// This is bad, it returns a Promise<int> even after being awaited
const mappedValue: Promise<number> = await Async.ofPromise(getSomethingAsync())
    .map(parseIntAsync)
    .get()

// This is good, it binds the promise returned from `parseIntAsync`
const value: number = await Async.ofPromise(getSomethingAsync())
    .bind(parseIntAsync) 
    .get()

console.log(value) // 5

Just like the Result monad, Async also has curried functions:

async function getName() {
    return "joshua"
}

function capitalize(str: string) {
    const first = str[0];
    const rest = str.slice(1);

    return first.toUpperCase() + rest.toLowerCase();
}

async function formatMessage(name: string) {
    return `Hello, ${name}!`
}

// Pipe the functions together to create the message, using the Async monad's currying
const message = await pipe(getName())
    .chain(Async.ofPromise)
    .chain(Async.map(capitalize))
    .chain(Async.bind(formatMessage))
    .chain(Async.get)
    .value();

console.log(message) // "Hello, Joshua!"