I tried to answer this initially but it was pointed out my original answer was not actually pointfree. I've taken another shot at making this pointfree and here's what I've ended up with:
import { constFalse, flow } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as R from "fp-ts/lib/Reader";
import * as M from "fp-ts/lib/Monoid";
import { fanOut } from "fp-ts/lib/Strong";
import { first } from "fp-ts/lib/Semigroup";
// Get an instance of fanOut for Reader
const fanOutImpl = fanOut(R.Strong, R.Category);
// Get an instance of a monoid that has the same behavior as `alt`
const firstMonoid = O.getMonoid<string>(first());
// A different _alternative_ would be to use the helpers from the
// Alternative module. I believe altAll(O.Alternative) is equivalent to
// the above code.
const normalParams = new URLSearchParams("normal=yes");
const otherParams = new URLSearchParams("otherNormal=yes");
const getFromNormal = (params: URLSearchParams): O.Option<string> =>
O.fromNullable(params.get("normal"));
const getFromOther = (params: URLSearchParams): O.Option<string> =>
O.fromNullable(params.get("otherNormal"));
// Used `flow` to get fully pointfree style
const isNormal: (params?: URLSearchParams) => boolean = flow(
O.fromNullable,
O.chain<URLSearchParams, string>(
flow(fanOutImpl(getFromNormal, getFromOther), M.concatAll(firstMonoid))
),
O.map((s) => s === "yes"),
O.getOrElse(constFalse)
);
console.assert(isNormal(normalParams) === true);
console.assert(isNormal(otherParams) === true);
console.assert(isNormal(undefined) === false);
I'm using the Reader
typeclass with fanOut
to get the behavior of calling multiple functions with a single input (in this case params
). Then the output of that is passed into the Monoid
concatAll
helper which will defines how to collect up the values from that result into a single value. Here I specified first
which has the same behavior as alt
(the first Some
value will be returned)
Also, fanOut
only works with two functions in this case which may not scale. One option would be to make a helper for your specific situation like:
// Add a helper for fanning out over an array
const fanAll = <A, B>(arr: Array<(a: A) => B>) => (a: A): B[] => pipe(
arr,
A.map((f) => f(a))
);
const isNormal2: (params?: URLSearchParams) => boolean = flow(
O.fromNullable,
O.chain<URLSearchParams, string>(
flow(fanAll([getFromNormal, getFromOther, getFromThird]), M.concatAll(firstMonoid))
),
O.map((s) => s === "yes"),
O.getOrElse(constFalse)
);
There is a difference between this and the original code as written which is that fanOut
will eagerly call each of the getFrom*
functions to get an Option
result for each and then use the Monoid logic to crunch those down into a single value. O.alt
will only run subsequent code if the code above it is None
. This behavior doesn't affect the runtime complexity, but it might still be suboptimal.
To achieve the same laziness behavior, you're going to have to do something like:
const altMonoid: M.Monoid<() => O.Option<string>> = {
empty: constant(O.none),
concat: (a, b) => flow(a, O.alt(b))
};
function apply<R>(f: () => R) {
return f();
}
function apply1<A, R>(arg: A) {
return (f: (a: A) => R) => f(arg);
}
function alt(
ins: Array<(params: URLSearchParams) => () => O.Option<string>>
): (p: URLSearchParams) => O.Option<string> {
return (p) => pipe(ins, A.map(apply1(p)), M.concatAll(altMonoid), apply);
}
function lazy<Args extends any[], R>(f: (...args: Args) => R) {
return (...args: Args) => () => f(...args);
}
const isNormal3: (params?: URLSearchParams) => boolean = flow(
O.fromNullable,
O.chain<URLSearchParams, string>(
pipe(
[getFromNormal, getFromOther, getFromThird],
A.map(lazy),
alt
)
),
O.map((s) => s === "yes"),
O.getOrElse(constFalse)
);
console.assert(isNormal3(normalParams) === true);
console.assert(isNormal3(otherParams) === true);
console.assert(isNormal3(undefined) === false);
But this is getting a bit complicated so I think I would recommend one of the first two options unless you really need the code to be pointfree and have the same laziness profile as the version with O.alt
.