How to do error accumulation with Cats Effect effects?

473 views Asked by At

I've been struggling with error accumulation with effects, and I was wondering what the idiomatic way of modeling this is using Cats Effect.

Without involving effects, I would do something like this:

import cats._
import cats.data._
import cats.implicits._
import cats.effect._

opaque type Version = String
opaque type Path = String
case class ExampleConfig(version: Version, path: Path)

enum ValidationError:
  case InvalidVersion
  case InvalidPath

// Implementation irrelevant to the example
def validateVersion(version: String): ValidatedNec[ValidationError, Version] = ???
def validatePath(path: String): ValidatedNec[ValidationError, Path] = ???

def validateConfig(version: String, path: String): ValidatedNec[ValidationError, ExampleConfig] =
  (validateVersion(version), validatePath(path)).mapN(ExampleConfig.apply)

However, if we now imagine validatePath actually verifies whether the path exists on the filesystem, it becomes an effect like Sync. This makes this case significantly more complex, as we now have:

  • an additional error channel that we don't want to use, but do need to deal with
  • nested types, which quickly become a hassle when mixing effectful and regular validations and when there's dependencies between them, requiring some type juggling
// Implementation irrelevant to the example
def pathExists[F[_]: Sync](path: Path): F[Boolean] = ???

def validatePath2[F[_]: Sync](path: String): F[ValidatedNec[ValidationError, Path]] =
  // Such a dependency between validations leads to major painpoints,
  // as we now have an effect inside a validation, while we would want it the other way around
  validatePath(path)
    .fold(
      e => Sync[F].delay(e.invalid),
      path => Sync[F].ifF(pathExists(path))(path.validNec, ValidationError.InvalidPath.invalidNec)
    )

def validateConfig2[F[_]: Sync](version: String, path: String): F[ValidatedNec[ValidationError, ExampleConfig]] =
  // We now have a distinction between our regular validations, and effectful validations
  val versionValid: ValidatedNec[ValidationError, Version] = validateVersion(version)

  for {
    pathValid: ValidatedNec[ValidationError, Path] <- validatePath2(path)
      // We need to deal with any errors in Syncs error channel,
      // in addition to our validation error domain,
      // as there technically can be additional errors in here that we want to map to validation errors
      .handleError(e => ValidationError.InvalidPath.invalidNec)
  } yield (versionValid, pathValid).mapN(ExampleConfig.apply)

Is there a better, idiomatic, way of modeling error accumulating effect using Cats Effect?

0

There are 0 answers