Skip to content

GheorgheP/fp-utils

Repository files navigation

FP Utils

Yes, this is another fp library for js. On the other and, this library doesn't try to compete with libraries like fp-ts. This library is a series of utility functions to make the development process easier.

Motivation

There are a lot of fp oriented libraries for js, but mostly on one hand they try to bring the entire FP world in js (but js/ts is not Haskell), this making harder to follow the flow; or on the other hand implement the fp concepts in object oriented style, where Just and Nothing instances have a private __value and a bunch of methods (map, apply, isNothing, isJust, ...) and the value is wrapped in this Just wrapper. While mentioned methods are not bad at all, and I even consider them good; the problem is that I find them pretty hard to be integrated in an existing code base, and for developers that are not familiarised with functional programming, hard to reason about. The FP Utils intention is to provide some simple abstractions that can be very easy integrated with an already existing codebase.

The definition of bottom

While the library want to be very lightweight and easy to use in existing codebase, it does come with a very specific behavior. The undefined and null are considered as same value and both are considered as bottom values. This is important to know and take in consideration when using this library, as many codebases tend to consider null as value and you may introduce many issues in your codebase using this library in your codebase.

Solution

The good part is that this definition is centralized in the Nothing type definition. So you can fork this repository and just redefine the Nothing meaning, by removing the null from definition. In some specific codebases, values like "" are considered as Nothing too, so you can extend the meaning of Nothing like you want.

Where is Just?

The library uses intensively the all known Maybe monad, but in same time, there is no instance of Nothing and Just. Any value tht is not Nothing (or in other words, is not undefined or null) is considered as Just. No value is wrapped in a Just layer.

Docs

Contents

Nothing

Nothing describes the null and undefined values. In this library they are considered as the same value.

import { Nothing } from "fp-utitlities/Nothing";

const asNull: Nothing = null;
const asUndefined: Nothing = undefined;

MNothing

MNothing<T> is a generic type that describes whenever a value may be Nothing or not.

import { MNothing } from "fp-utitlities/Nothing";

type MString = MNothing<string>

const asNull: MString = null;
const asUndefined: MString = undefined;
const asString: MString = "test";

isNothing

Check is the value is a Nothing value

[ codesandbox ]

import { isNothing, MNothing } from "fp-utitlities/Nothing";

const ls: Array<MNothing<number>> = [1, undefined, 3, null, 4];

const emptiesCount = ls.filter(isNothing).length; // 2

isT

Check whenever a potential maybe value is empty or not

[ codesandbox ]

import { isT } from "fp-utitlities/Nothing";

const inc = (a: number): number => a + 1

const ls: Array<number|undefined> = [1, undefined, 3, undefined, 4];
const incremented = ls.filter(isT).map(inc); // [2, 4, 5]

orElse

Return the default value if the provided value is Nothing Note: the type of the default value needs to be the same as the type of the checked value.

orElese is already curried, so you can use it in both full applied or partial applied form.

[ codesandbox ]

import { orElse } from "fp-utilities";

const ls: Array<string | undefined> = ["😁", undefined, "😁", undefined, "😁"];
const fillWithSmiles = ls.map(orElse("😱"));

console.log(fillWithSmiles); // ["😁", "😱", "😁", "😱", "😁"];

pass

Provide a predicate and a value for it. If predicate returns true, return back that value. Otherwise, return undefined. Note: pass support both simple boolean predicates and type guards. So you get full type checker support. pass is very handy in combination with mPipe, when you want to guard the pipe with a predicate.

pass is already curried, so you can use it in both full applied or partial applied form.

[ codesandbox ]

import { pass, mPipe } from "fp-utilities";

type Mood = "good" | "bad";

const isMood = (s: string): s is Mood => ["good", "bad"].includes(s as Mood);
const isGood = (m: Mood): m is "good" => m === "good";
const isBad = (m: Mood): m is "bad" => m === "bad";

// Read a "good" value from a string
const goodMoodGalsses = mPipe(pass(isMood), pass(isGood));

// Read a "bad" value from a string
const badMoodGalsses = mPipe(pass(isMood), pass(isBad));

console.log(goodMoodGalsses("no mood")); // undefined
console.log(goodMoodGalsses("bad")); // undefined
console.log(goodMoodGalsses("good")); // "good"

console.log(badMoodGalsses("no mood")); // undefined
console.log(badMoodGalsses("good")); // undefined
console.log(badMoodGalsses("bad")); // "bad"

liftA2

Apply a binary function over result of 2 single functions.

[ codesandbox ]

import { liftA2 } from "fp-utilities";

const sum = (a: number, b: number): number => a + b;
const inc = (a: number): number => a + 1;
const double = (a: number): number => a * 2;

const fn = liftA2(sum, inc, double);

console.log(fn(2, 3)); // 9

mPipe

Apply a binary function over result of 2 single functions.

[ codesandbox ]

import { mPipe } from "fp-utilities";

type T = {
  a?: {
    b?: {
      c?: {
        d?: "bingo";
      };
    };
  };
};

const readBingo: (t: T) => "bingo" | undefined = mPipe(
  (v: T) => v.a,
  (v) => v.b,
  (v) => v.c,
  (v) => v.d
);

console.log(readBingo({})); // undefined
console.log(readBingo({ a: {} })); // undefined
console.log(readBingo({ a: { b: {} } })); // undefined
console.log(readBingo({ a: { b: { c: {} } } })); // undefined
console.log(readBingo({ a: { b: { c: { d: undefined } } } })); // undefined
console.log(readBingo({ a: { b: { c: { d: "bingo" } } } })); // "bingo"

match

Provide a series of type guard predicates that satisfies the input, and a function that will resolve the value if it matches the type guard. In other words this is a type safe if else statement. In case the provided type guards list doesn't cover the entire input type, you'll get a type error at compile time.

Important: match function requires ts strictFunctionTypes or strict compiler option to be enabled.

[ codesandbox ]

Let's pretend that we have a T type, that is an union of A, B and C. We need the function fn that will return a distinct string per input T, for A -> "a", B -> "b", C -> "c". In case A, B, C are primitive type like number or string, we can use switch statement to safely rezolve the constraints. But in case A, B, C are more complex types, like products or unions, switch will be a no go, and we are forced in using the if else statement.

type A = { a: string };
const isA = (t: T): t is A => "a" in t;

type B = { b: number };
const isB = (t: T): t is B => "b" in t;

type C = { c: string[] };
const isC = (t: T): t is C => "c" in t;

type T = A | B | C;

const fn = (v: T): string => {
  if (isA(v)) {
    return "a";
  } else if (isB(v)) {
    return "b";
  } else {
    return "c";
  }
};

Now let's pretend that we extend the T type, by adding the D type. Now we run in trouble as we may not know that somewhere we have a function fn that should return a distinct output for each A, B, C or D input. Also the type checker will not inform us at the compile time, so we will endup with a silent bug on production.

Here match comes handy. It just uses the power of type guards in order to detect if all cases are treated.

import { match } from "fp-utilities";

type A = { a: string };
const isA = (t: T): t is A => "a" in t;

type B = { b: number };
const isB = (t: T): t is B => "b" in t;

type C = { c: string[] };
const isC = (t: T): t is C => "c" in t;

type T = A | B | C;

const fn: (ab: T) => string = match(
  [isA, () => "a"],
  [isB, () => "b"],
  [isC, () => "c"]
);

console.log(fn({ a: "a" })); // "a"
console.log(fn({ b: 1 })); // "b"
console.log(fn({ c: [] })); // "c"

Now in case we add the type D to T, compiler will give us a compile time error, as not all cases are matched.

Note: Don't provide type guards that matches entire type, as in this case match will work in same way as if else statement.

match2

Provide a series of type guard predicates that satisfies the input, and a function that will resolve the value if it matches the type guard.

This function is similar to match, but resolves 2 arguments input cases.

[ codesandbox ]

!Note: There is a big difference between match and match2 does not force you to provide all combinations, so it can return undefined, as return type mentions. There is technical and coding experience limitation. In order to make match2 be strict as match, the use will have to provide all possible cases between first argument, and the second one. This is already cartesian product, and it can get huge very easy. For 3x3 input, you get 9 combinations. And yes, you guessed it, for 4x4, 16 combinations. In real world usually you need 4 combinations. But at the same moment it enforces you to satisfy entire input type in at least one of type guards. And this is what makes it different from if else statement.

Important: match2 function requires ts strictFunctionTypes or strict compiler option to be enabled.

With match2 you can very easy create type safe reduxjs reducers.

// region State
type Red = "Red";
const red = (): Red => "Red";
const isRed = (s: State): s is Red => s === "Red";

type RedYellow = "RedYellow";
const redYellow = (): RedYellow => "RedYellow";
const isRedYellow = (s: State): s is RedYellow => s === "RedYellow";

type Yellow = "Yellow";
const yellow = (): Yellow => "Yellow";
const isYellow = (s: State): s is Yellow => s === "Yellow";

type Green = "Green";
const green = (): Green => "Green";
const isGreen = (s: State): s is Green => s === "Green";

type State = Red | RedYellow | Yellow | Green;
// endregion

// region Actions
type Switch = Action<"switch">;
const _switch = (): Switch => ({ type: "switch" });
const isSwitch = (a: Actions): a is Switch => a.type === "switch";

type Actions = Switch;
// endregion

// region Reducer
import { Reducer } from "redux";
import { match2 } from "fp-utilities";

export const _reducer: (
  s: State.State,
  a: Actions.Actions
) => State.State | undefined = match2(
  [State.isRed, Actions.isSwitch, State.redYellow],
  [State.isRedYellow, Actions.isSwitch, State.green],
  [State.isGreen, Actions.isSwitch, State.yellow],
  [State.isYellow, Actions.isSwitch, State.red]
);

export const reducer: Reducer<State.State, Actions.Actions> = (
  s = State.red(),
  a
) => _reducer(s, a) ?? s;
// endregion

Last 2 guards are enforced by match2, as you need to handle all state instances at leas in one guard. Sure, this reducer may return undefined, but you can handle this very easy in parent reducer.

parse

Parse for a object structure from an type A. Need to provide a parser function for each object key. If any of the parsers will return undefined, the parsing process will be stopped and will return undefined. !Important: If the key is optional, you are forced to tell explicitly if the parser is strict or optional.

  • optional - even if this parser will return undefined, the parsing process will not be stopped, as the key is optional by specs and it is allowed to be undefined.

[ codesandbox ]

import { parse, optional } from "fp-utilities";

interface Author {
  name?: string;
  age?: number;
  phone?: string;
  email?: string;
}

interface User {
  name: string;
  meta: {
    email?: string;
    phone?: string;
    age?: number;
  };
}

const fromAuthor = parse<Author, User>({
  name: (a) => a.name,
  meta: parse<Author, User["meta"]>({
    email: optional((v) => v.email),
    phone: optional((v) => v.phone),
    age: optional((v) => v.age)
  })
});

console.log(
  fromAuthor({
    name: "John Doe",
    age: 50,
    phone: "34-56-78",
    email: "john.doe@gmail.com"
  })
); // User

console.log(
  fromAuthor({
    name: undefined,
    age: 50,
    phone: "34-56-78",
    email: "john.doe@gmail.com"
  })
); // undefined

parseStrict

Parse for a object structure from an type A. Need to provide a parser function for each object key. The difference between parse, is that all provided parser functions need to return always a strict value, and should not return undefined. !Important: If the key is optional, you are forced to tell explicitly if the parser is strict or optional.

  • optional - even if this parser will return undefined, the parsing process will not be stopped, as the key is optional by specs and it is allowed to be undefined.

[ codesandbox ]

import { parse, optional } from "fp-utilities";

interface Author {
  name?: string;
  age?: number;
  phone?: string;
  email?: string;
}

interface User {
  name: string;
  meta: {
    email?: string;
    phone?: string;
    age?: number;
  };
}

const fromAuthor = parse<Author, User>({
  name: (a) => a.name,
  meta: parse<Author, User["meta"]>({
    email: optional((v) => v.email),
    phone: optional((v) => v.phone),
    age: optional((v) => v.age)
  })
});

console.log(
  fromAuthor({
    name: "John Doe",
    age: 50,
    phone: "34-56-78",
    email: "john.doe@gmail.com"
  })
); // User

console.log(
  fromAuthor({
    name: undefined,
    age: 50,
    phone: "34-56-78",
    email: "john.doe@gmail.com"
  })
); // undefined

or

Provide a series of functions and return the result of the first function that does not return Nothing.

Let's take the example from match2 where we create the reducer, but we need to avoid the case of returning undefiend. With or this problem is solved very easy and nice, as we provide the second that will just return the current state.

import { Reducer } from "redux";
import { or, match2 } from "fp-utitlities";

export const _reducer: (s: State.State, a: Actions.Actions) => State.State = or(
  match2(
    [State.isRed, Actions.isSwitch, State.redYellow],
    [State.isRedYellow, Actions.isSwitch, State.green],
    [State.isGreen, Actions.isSwitch, State.yellow],
    [State.isYellow, Actions.isSwitch, State.red]
  ),
  state => state,
);

Another good usage is in combination with parsers, when you want to parse for a union type, where constructors have different structure, and you need parse one bby one until you match the right one.

[ codesandbox ]

import { parse, mPipe, or } from "fp-utilities";

const readNumber = (v: unknown): number | undefined => (typeof v === "number" && !isNaN(v) ? v : undefined);
const prop = <K extends keyof T, T extends Record<any, any>>(k: K) => (t: T): T[K] => t[k];

interface Square {
  length: number;
}

const parseSquare = parse<Record<string, unknown>, Square>({
  length: mPipe(prop("length"), readNumber),
});

interface Rectangle {
  width: number;
  height: number;
}
const parseRectangle = parse<Record<string, unknown>, Rectangle>({
  width: mPipe(prop("width"), readNumber),
  height: mPipe(prop("height"), readNumber),
});

type Figure = Square | Rectangle;

const parseFigure = or(parseSquare, parseRectangle);

console.log(parseFigure({ length: 45 })); // With return a Square
console.log(parseFigure({ width: 45, height: 45 })); // With return a Rectangle
console.log(parseFigure({ width: 45 })); // With return a `undefined`

About

Some util functions for functional approach

Resources

Stars

Watchers

Forks

Packages

No packages published