Scalaz 7: Idiomatic way of turning values in Either to plain values plus logged errors?
Question
Given a function f: A => E \/ B
, what is an idiomatic way to gather the B
results for some list of A
s while logging the E
s?
I came up with the following (partially while answering this SO question):
import scalaz._, Scalaz._
type Logger[+A] = Writer[List[String], A]
def logged[A, E, B, F[_]](f: A => E \/ B)(implicit FM: Monoid[F[E]], FP: Pointed[F]): (A => Writer[F[E], Option[B]]) =
(a: A) => f(a).fold(e => Writer(FP.point(e), None), b => Writer(FM.zero, Some(b)))
def listLogged[A, E, B](f: A => E \/ B) = logged[A, E, B, List](f)
type W[+A] = Writer[List[String], A]
def keepOdd(n: Int): String \/ Int =
if (n % 2 == 1) \/.right(n) else \/.left(n + " was even")
scala> val x = List(5, 6).traverse[W, Option[Int]](listLogged(keepOdd))
x: W[List[Option[Int]]] = scalaz.WriterTFunctions$$anon$26@503d0400
scala> x.run
res11: (List[String], List[Option[Int]]) = (List(6 was even),List(Some(5), None))
scala> val (logs, results) = x.map(_.flatten).run
logs: List[String] = List(6 was even)
results: List[Int] = List(5)
Is there a shorter / better / less restrictive / more general way?
Solution
You can use putWith
to write what to my eye is a more readable logged
method:
def logged[A, E, B, F[_]: PlusEmpty: Pointed](f: A => E \/ B) = (a: A) =>
WriterT.putWith(f(a).point[Id])(_.swap.toOption.orEmpty[F]).map(_.toOption)
I also think the PlusEmpty
context bound on F
looks a little cleaner than explicitly requiring F[E]]
to be a monoid (it accomplishes the same thing, of course). It's a shame that the .point[Id]
bit is necessary—that there's not a Writer.putWith
—but beggars can't be choosers, I guess.
I'd also write keepOdd
like this:
def keepOdd(n: Int) = Either.cond(n % 2 == 1, n, n + " was even").disjunction
Or at least use n.right
instead of \/.right(n)
, but that's just a matter of taste.