3

Given a value, I'd like to pass it through two functions each which would return an Option. I'd like to use the first Some which is returned.

To do this, I currently use O.alt like so:

Slightly contrived example:

import { constFalse, pipe } from "fp-ts/function";
import * as O from "fp-ts/Option";

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"));

const isNormal = (params?: URLSearchParams): boolean =>
  pipe(
    params,
    O.fromNullable,
    O.chain<URLSearchParams, string>((p) =>
      pipe(
        getFromNormal(p),
        O.alt(() => getFromOther(p))
      )
    ),
    O.map((s) => s === "yes"),
    O.getOrElse(constFalse)
  );

console.assert(isNormal(normalParams) === true);
console.assert(isNormal(otherParams) === true);
console.assert(isNormal(undefined) === false);

I would love to be able to replace that O.chain section with something more along the lines of:

    O.chain<URLSearchParams, string>(
      O.alt(getFromNormal, getFromOther)
    ),

But obviously O.alt does not work in this way. But is there another type of function I can use to achieve a pointfree approach to this?

I Stevenson
  • 854
  • 1
  • 7
  • 24

2 Answers2

1

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.

Souperman
  • 5,057
  • 1
  • 14
  • 39
  • 1
    What a really insightful and thorough response, thank you! I'm not across `fanOut` - this is the first time I've come across it. And more so I'm only vaguely aware of `Reader` - but semigroups and monoids are fine and allow me to get the basic idea here. If you're able to further explain why the need for Reader here that could be handy - or links to further understand that'd be great. Also you're highlighting of the lazy nature of `O.alt` is interesting too, and gives me some things to think about. Thanks! – I Stevenson Sep 26 '22 at 02:24
  • 1
    `Reader` as a type class has some interesting helpers built out for it, but if you look at the interface definitions it's very close to just a regular function. I needed something that implemented `Strong` (which is a further refinement of `Profunctor`) and `Reader` fit the bill. For more info, the creator of the library wrote a post about `Reader`: https://dev.to/gcanti/getting-started-with-fp-ts-reader-1ie5 – Souperman Sep 26 '22 at 03:20
0

But is there another type of function I can use to achieve a pointfree approach to this?

You could use constant from fp-ts/function to write the code in a pointfree way, but it will have slightly different behavior:

const isNormal = (params?: URLSearchParams): boolean =>
  pipe(
    params,
    O.fromNullable,
    O.chain<URLSearchParams, string>((p) =>
      pipe(getFromNormal(p), O.alt(constant(getFromOther(p))))
    ),
    O.map((s) => s === "yes"),
    O.getOrElse(constFalse)
  );

The reason this is going to have different behavior is that constant will take a value and wrap it up in a function that returns that value. For it to have a value, it has to eagerly evaluate the getFromOther(p). So long as that's ok, then this approach works, but there isn't going to be a pointfree way to write this code that is lazily evaluated without an anonymous function slipping in somewhere.

An alternative option would be to define a helper:

function lazy<T, Args extends any[]>(f: (...args: Args) => T) {
  return (...args: Args) => () => f(...args);
}

// Or this if you prefer to write code in a less "curried" way.
function lazy2<T, Args extends any[]>(f: (...args: Args) => T, ...args: Args) {
  return () => f(...args);
}

Which you can then use in a pointfree way and still avoid eager evaluation:

const isNormal = (params?: URLSearchParams): boolean =>
  pipe(
    params,
    O.fromNullable,
    O.chain<URLSearchParams, string>((p) =>
      pipe(getFromNormal(p), O.alt(lazy(getFromOther)(p)))
    ),
    O.map((s) => s === "yes"),
    O.getOrElse(constFalse)
  );

Souperman
  • 5,057
  • 1
  • 14
  • 39
  • Thank you for the answer, but alas the code you've provided is _not_ pointfree. Note how you still need to make mention of `p` to each function call. – I Stevenson Sep 21 '22 at 00:03
  • oops you're totally right. I got overly focused on trying to remove the anonymous function from the `alt` that I lost sight of the bigger picture. I believe there is a way to do what you want so I'll take a crack at it but I think it's going to be slightly complicated to do with only `fp-ts` helpers (my current idea involves `fanOut` from `Strong`) or else using `ap`. A one off helper might not be the worst option – Souperman Sep 21 '22 at 04:18