0

I have two validation functions for different values that return Either. I'd like to throw an exception if one of them has a left value and do nothing if both are right. I've never used fp-ts before and can't figure out how to properly combine left results. My current solution works but does not feel like i'm using it properly.

import { Either, left, right, isLeft, getOrElse } from 'fp-ts/lib/Either';

function validateMonth( m: Month ): Either<Error, Month> {
   return m.isInRange() ? right(m) : left(new Error('Month must be in range!'));
}

function validateYear( y: Year ): Either<Error, Year> {
   return year.isBefore(2038) ? right(y) : left(new Error('Year must be before 2038!'));
}

function throwingValidator(m: Month, y: Year): void {
 // todo: Refactor to get rid of intermediate variables,
 // combining results of validateMonth and validateYear into a type
 // of Either<Error, Unit>
 const monthResult = validateMonth( month );
 const yearResult = validateYear( year );
 const throwOnError = (e: Error) => { throw e; };
 if ( isLeft( monthResult ) ) { getOrElse(throwOnError)(monthResult); }
 if ( isLeft( yearResult ) ) { getOrElse(throwOnError)(yearResult); }
}

I've read the introduction at https://dev.to/gcanti/getting-started-with-fp-ts-either-vs-validation-5eja but that code is exactly the opposite of what I want: I don't care about the input value after the validation and want to return only the first error that occurs.

chiborg
  • 26,978
  • 14
  • 97
  • 115
  • `getOrElse` is used to get a result value, but you're not doing anything with its return value - why do you even call it? And especially why call it only when you've already established that it's a `Left` value? That said, throwing exceptions is not really a good practice in functional programming, so it's naturally a bit hard. – Bergi Sep 18 '19 at 21:10
  • 1
    If you don't care about the return value, `Either` is not the right choice for the return type. Or at least, not `Either` - at best you'd do `Either`. (Where in TypeScript, `Unit` is either `void` or `undefined` or `null`). – Bergi Sep 18 '19 at 21:13
  • I'm misusing `getOrElse` to call a function for the error case. But you're right about exceptions - what I really want to do is capture the `Error` object and return it in a rejected promise. When all values are correct, i want to return a Promise with a specific data structure. – chiborg Sep 18 '19 at 21:42
  • Well at least you shouldn't need both of `isLeft` and `getOrElse` - one of them would be enough. – Bergi Sep 18 '19 at 21:44
  • People might also check out this example: https://codesandbox.io/s/array-of-validators-vxgj6?from-embed=&file=/src/arrayOfValidators.ts:110-153 – ANimator120 May 11 '21 at 05:30

2 Answers2

2

You're probably looking for something like

const toPromise = fold(e => Promise.reject(e), r => Promise.resolve(r));

Promise.all([
    toPromise(validateMonth(month)),
    toPromise(validateYear(year)),
]).then(([validMonth, validYear]) => {
    return …
});

or the more functional way

toPromise(ap(ap(of(validMonth => validYear => {
    return …
}), validateMonth(month)), validateYear(year)))

You can also do the Promise.all with array.sequence and the toPromise afterwards.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • @bob For some reason I thought that `map` did have the wrong (flipped) signature, and I totally missed `liftA2`. – Bergi Sep 19 '19 at 11:24
  • @Bergi why introducing `Promise` here? – Giovanni Gonzaga Sep 23 '19 at 09:29
  • @GiovanniGonzaga OP wrote (in a comment) "*When all values are correct, i want to return a Promise with a specific data structure*". Using rejected promises as a return value is easier than throwing. – Bergi Sep 23 '19 at 12:12
  • I see, I got confused because `Promise`/async is not mentioned in the original question. Re "rejected promises as a return value is easier than throwing", the equivalent fp-ts encoding would be a `TaskEither` which is a `() => Promise>` (lazy because promises being eager break referential transparency) – Giovanni Gonzaga Sep 23 '19 at 12:15
2

Ignoring throwingValidator and throwing in general (which kind of defeats the purpose of using fp-ts in the first place) and focusing only on this specific request:

Refactor to get rid of intermediate variables, combining results of validateMonth and validateYear into a type of Either

You are probably looking for:

const monthAndYearResult: Either<
  Error,
  { month: Month, year: Year }
> = sequenceS(either)({
  month: validateMonth(month),
  year: validateYear(year)
})

"sequence"ing in general requires an instance of Traversable (the struct { year, month } in this case) and an instance of Applicative (either in this case), and the semantics are the ones of aggregating different independent computations together.

If you explicitly want to ignore the result, usually a _-suffix alternative is provided to accomplish this, but it isn't as of now in fp-ts v2.

To obtain a Either<Error, void> you can thus resort to:

const result = pipe(
  sequenceS(E.either)({ month: validateMonth(month), year: validateYear(year) }),
  E.map(constVoid)
)

Note that sequenceS is just one of the possible options, you could use sequenceT or array.sequence to obtain similar results, e.g.:

pipe(
  sequenceT(either)([validateMonth(month), validateYear(year)]),
  E.map(constVoid)
)
Giovanni Gonzaga
  • 1,185
  • 9
  • 8