functional-programmingapplicativeeitherfp-ts

Traverse/sequence an Array<Either<E, A>> into Either<Array<E>, Array<A>> in fp-ts


I have a list of entities where even one failed validation would yield an error. However, I'd still like to iterate the whole list and collect all the errors for further logging.

Traverse/sequence with a default Either's Applicative would yield Either<E, A[]> (only the first encountered error) instead of needed Either<E[], A[]>.

Is there a default tool in the library to achieve this, or could it be achieved by writing a custom Applicative?


Solution

  • There’s no fp-ts function that does this directly, but you could use getApplicativeValidation from the Either module:

    import * as E from 'fp-ts/lib/Either'
    import * as RA from 'fp-ts/lib/ReadonlyArray'
    import {pipe} from 'fp-ts/lib/function'
    
    const collectErrors = <E, A>(
      xs: readonly E.Either<E, A>[]
    ): E.Either<readonly E[], readonly A[]> =>
      pipe(
        xs,
        RA.traverse(E.getApplicativeValidation(RA.getSemigroup<E>()))(
          E.mapLeft(RA.of)
        )
      )
    
    // Left(['a', 'b'])
    collectErrors([E.left('a'), E.right(1), E.left('b'))])
    // Right([1, 2])
    collectErrors([E.right(1), E.right(2)])
    // Right([])
    collectErrors([])
    

    How this works is that getApplicativeValidation takes a Semigroup instance and returns an Applicative instance that can be used with traverse. The applicative instance will combine errors using the semigroup instance.

    RA.traverse(
      // The applicative instance for the either that will collect the errors 
      // into an array
      E.getApplicativeValidation(
        // The semigroup instance for readonly E[], which concatenates the arrays
        RA.getSemigroup<E>()
      )
    )(
      // Turn each Either<E, A> into Either<readonly E[], A> by putting each error
      // inside an array so they can be concatenated
      E.mapLeft(RA.of)
    )