0

I'm attempting to validate an operation of applying a command to an array representing svg path data using fp-ts.

type CommandValidation = (commands: CommandArray, nextCommand: Command) => option.Option<string>;

const newCommandValidations: Array<CommandValidation> = [
  validateFirstCommandIsMove,
  validateSymmetricCommandFollowsBezier,
  validateNoMoveAfterMove,
];

export const safelyPushCommand = (command: Command) => either.map((commands: CommandArray) => {
  // the errors don't overlap yet but in future errors may co-exist
  const validationErrors = fpFunction.pipe(newCommandValidations,
    fpArray.map((validation) => validation(commands, command)),
    fpArray.filter(option.isSome),
    fpArray.map(option.fold(() => undefined, (some) => some)));

  if (validationErrors.length > 0) {
    return either.left(validationErrors);
  }
  return either.right(pushCommands([command])(commands));
});

Unfortunately the validationErrors is still regarded as a list of items which could have a none in them.

How can I get a array of strings (typed as such) representing any validation errors for this operation.

Should I be using an "apply" function to put the parameters into the validation functions?

How can I get an apply function for the validators?

Is there a better way to do the fold operation when the second function is just the identity function? EDIT: I had this question answered in this video https://youtu.be/1LCqHnaJJtY?t=2470 : option.fold(() => undefined, (some) => some) === option.getOrElse(() => undefined) === option.toUndefined

Lots of questions from someone lost in the jungle of fp-ts, hope you can provide some insight to help my project become useful. The example code is contained in a branch I cut here: https://github.com/justin-hackin/fp-ts-svg-path-d/tree/validators

J. Barca
  • 538
  • 6
  • 19
  • Need to clarify a few things. Does a `CommandValidation` function return `Some` when the validation fails? Also, what is the `pushCommands` function? – cdimitroulas Jul 01 '21 at 14:02
  • You are correct, returns Some when there is error. pushCommands is here https://github.com/justin-hackin/fp-ts-svg-path-d/blob/d9fb881e6616b662527d2e2de64d1cb949e7f123/src/lib/command.ts#L152, essentially a concat for commands – J. Barca Jul 09 '21 at 14:08

1 Answers1

2

Here are some improvements to the code you provided, happy to update this if necessary once I get a response to my comment about some of the details that were unclear.

import * as either from 'fp-ts/lib/Either'
import * as fpFunction from 'fp-ts/lib/function'
import * as option from 'fp-ts/lib/Option'
import * as fpArray from 'fp-ts/lib/Array'

// Didn't know what these types should be so I kept them as basic as possible
// to allow me to compile the code
type CommandArray = unknown[];
type Command = unknown;

type CommandValidation = (commands: CommandArray, nextCommand: Command) => option.Option<string>;

// Declaring these consts here just to allow me to write the code below
declare const validateFirstCommandIsMove: CommandValidation
declare const validateSymmetricCommandFollowsBezier: CommandValidation
declare const validateNoMoveAfterMove: CommandValidation

const newCommandValidations: Array<CommandValidation> = [
  validateFirstCommandIsMove,
  validateSymmetricCommandFollowsBezier,
  validateNoMoveAfterMove,
];

export const safelyPushCommand = (command: Command) => (commands: CommandArray) => {
  return fpFunction.pipe(
    newCommandValidations,
    fpArray.map((validation) => validation(commands, command)),
    option.sequenceArray, // Converts Array<Option<T>> -> Option<Array<T>>
    option.fold(
      // if we have a None, it means no validations returns an validation error message, so we succeed
      () => either.right(pushCommands([command])(commands)),
      // if we have a Some, we fail with the validation errors
      (validationErrors) => either.left(validationErrors)
    )
  );
};

The above is just me trying to adapt your code to get it more or less working. However, the code is not very idiomatic.

Firstly, the convention for Option is that None is used to indicate some kind of failure, or absence of a value. In your code, you use None to indicate that a validation passed but a Some to indicate that a validation failed which feels a little bit counterintuitive. I would suggest to use an Either instead, where the Left side is used to hold validation errors.

Since Either is designed to short-circuit after the first failure but you want to collect all the validation errors, we need another technique for this. There is a good article on this topic -> https://dev.to/gcanti/getting-started-with-fp-ts-either-vs-validation-5eja

The solution below is a small example using either.getApplicativeValidation (either.getValidation is now deprecated, so the article linked above is a bit out of date) to allow us to run all 3 of your validation functions and collect all the errors in a NonEmptyArray<string>.

import { sequenceT } from 'fp-ts/lib/Apply'
import { getSemigroup, NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'

declare const validateFirstCommandIsMove1: (command: Command) => either.Either<NonEmptyArray<string>, Command>;
declare const validateSymmetricCommandFollowsBezier1: (command: Command) => either.Either<NonEmptyArray<string>, Command>;
declare const validateNoMoveAfterMove1: (command: Command) => either.Either<NonEmptyArray<string>, Command>;

const runValidations = sequenceT(either.getApplicativeValidation(getSemigroup<string>()))

const validateCommand = (command: Command): either.Either<NonEmptyArray<string>, string> => {
  return fpFunction.pipe(
    command,
    runValidations(
      validateFirstCommandIsMove1,
      validateSymmetricCommandFollowsBezier1,
      validateNoMoveAfterMove1
    ),
  )
}
cdimitroulas
  • 2,380
  • 1
  • 15
  • 22
  • This is a very helpful response! I have not had a chance to work on this yet but I will accept it after I implement the solution. The part that really helped was the use of sequenceArray and it's even explained it in the comments, much appreciated! I know that the use of the option was kind of weird but I thought I could better preserve the idea of identity or error using these functions. Now I see you can run all the validations and get all the errors this way and even then do it for multiple applications of an array command so thee user sees all the syntax errors in their path at once. – J. Barca Jul 09 '21 at 03:08