2

Suppose I have a list of functions E => Either[Exception, Unit] to invoke upon event E and accumulate the errors to return Either[List[Exception], Unit].

type EventHandler = E => Either[Exception, Unit]

import cats.data.NonEmptyList

def fire(
  e: Event, 
  subscribers: List[EventHandler]
): Either[NonEmptyList[Exception], Unit] = ???

I'd like to implement fire with cats

 import cats.implicits._

 subscribers.foldMap (_ map (_.toValidatedNel))
            .map (.toEither)
            .apply(e)

Does it make sense ? How would you improve it ?
How to change fire to invoke subscribers concurrently ?

Michael
  • 41,026
  • 70
  • 193
  • 341

1 Answers1

5

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.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Thank you ! Why would you write with `Applicative` rather than `Monoid`. I guess `Applicative` is more generic and probably "more idiomatic" but `Monoid` seems simpler to me. Besides `Validated` has a `Monoid` instance after all. It has it for a reason. – Michael Mar 14 '17 at 20:55
  • @Michael It's a matter of taste, but "perform this operation on all these elements" feels more traverse-y to me. – Travis Brown Mar 14 '17 at 21:54
  • Isn't monoid the "least powerful abstraction" for this case, as it's explained in http://stackoverflow.com/a/19881777/521070 ? – Michael Mar 18 '17 at 08:10