I'd probably write it like this:
import cats.data.NonEmptyList, cats.implicits._
type Event = String
type EventHandler = Event => Either[Exception, Unit]
def fire(
e: Event,
subscribers: List[EventHandler]
): Either[NonEmptyList[Exception], Unit] =
subscribers.traverse_(_(e).toValidatedNel).toEither
(If you're not on 2.12.1 or are but can't use -Ypartial-unification
you'll need traverseU_
.)
If you want the calls to happen concurrently, normally you'd reach for EitherT[Future, Exception, _]
, but that's not going to give you the error accumulation you want. There's no ValidatedT
, but that's because Applicative
composes directly. So you could do something like this:
import cats.Applicative
import cats.data.{ NonEmptyList, ValidatedNel }, cats.implicits._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
type Event = String
type EventHandler = Event => Future[Either[Exception, Unit]]
def fire(
e: Event,
subscribers: List[EventHandler]
): Future[Either[NonEmptyList[Exception], Unit]] =
Applicative[Future].compose[ValidatedNel[Exception, ?]].traverse(subscribers)(
_(e).map(_.toValidatedNel)
).map(_.void.toEither)
(Note that if you're not using kind-projector you'll need to write out the type lambda instead of using ?
.)
And to prove to yourself that it's happening concurrently:
fire(
"a",
List(
s => Future { println(s"First: $s"); ().asRight },
s => Future { Thread.sleep(5000); println(s"Second: $s"); ().asRight },
s => Future { println(s"Third: $s"); ().asRight }
)
)
You'll see First
and Third
immediately.