How to pipe ap calls after using getApplicativeValidation on an Either in fp-ts?

Viewed 317

Yesterday me and a few colleagues were trying to get a toy example for applicative validation in fp-ts to work. We finally got it working with manually storing each interim step in a variable and calling the next step. But it would be way more elegant using the pipe function from fp-ts. Doing it with Either directly works, but does not combine multiple Left values into one (e.g. concatenating arrays with string errors). But with pipe() the ap()-calls want two arguments, but only get one. How do we use pipe correctly here:

import * as E from "fp-ts/lib/Either";
import * as RA from "fp-ts/lib/ReadonlyArray";
import { pipe } from "fp-ts/lib/function";

export class CreditCard {
    constructor(
        public readonly number: string,
        public readonly expiry: string,
        public readonly cvv: string
    ) { }
}

export const validate = (
    card: CreditCard
): E.Either<ReadonlyArray<string>, CreditCard> => {
    const createCreditCard = (a: string) => (b: string) => (c: string) =>
        new CreditCard(a, b, c);

    const v1 = (s: string): E.Either<ReadonlyArray<string>, string> => {
        return s !== "invalid" ? E.right(s) : E.left(RA.of("invalid number"));
    };

    const v2 = (s: string): E.Either<ReadonlyArray<string>, string> => {
        return s !== "invalid" ? E.right(s) : E.left(RA.of("invalid expiry"));
    };

    const v3 = (s: string): E.Either<ReadonlyArray<string>, string> => {
        return s !== "invalid" ? E.right(s) : E.left(RA.of("invalid cvv"));
    };

    const V = E.getApplicativeValidation((RA.getSemigroup<string>()));

    // this does not work, because V.ap wants 2 arguments but only has 1?
    // const fromPipe = pipe(
    //     V.of(createCreditCard),
    //     V.ap(v1(card.number)),
    //     V.ap(v2(card.expiry)),
    //     V.ap(v3(card.cvv))
    // );
    // return fromPipe;

    // this works, but is ugly
    const liftedFunction = V.of(createCreditCard);
    const afterFirstValidation = V.ap(liftedFunction, v1(card.number));
    const afterSecondValidation = V.ap(afterFirstValidation, v2(card.expiry));
    const afterThirdValidation = V.ap(afterSecondValidation, v3(card.cvv));

    return afterThirdValidation;
};
1 Answers

Either.getApplicativeValidation returns an instance of Applicative2C which has non-pipeable versions of the class methods. Currently, the way you get pipeable versions of instances computed with combinators (like getApplicativeValidation) is to pass the instance to the pipeable combinator from the pipeable.ts module.

So change your code to this:

const validation = E.getApplicativeValidation(RA.getSemigroup<string>())
const V = pipeable(validation)

const fromPipe = pipe(
  validation.of(createCreditCard),
  V.ap(v1(card.number)),
  V.ap(v2(card.expiry)),
  V.ap(v3(card.cvv))
)

And you should find it works as you wanted.

However, I believe in fp-ts v3.x.x, type class interfaces will be changed to all be pipeable by default, so the need for pipeable will go away then.

Related