scalamonadsfor-comprehension

Monadic way to get first Right to result from getting an Either from items of a list?


Up front: I know how to just write a custom function that will do this, but I swear there's a built-in thing whose name I'm just forgetting, to handle it in an idiomatic way. (Also, in my actual use case I'm likely to be using more complex monads involving state and assorted nonsense, and I feel like the answer I'm looking for will handle those as well, while the hand-coded one would need to be updated.)

I have a list items : List[A] and a function f : (A) -> Either[Error, B]. I vaguely recall there's an easy dedicated function that will apply f to each item in items and then return the first Right(b) to result, without applying f to the remaining items (or return Left[error] of the last error, if nothing succeeds.)

For example, if you had f(items(0)) result in Left("random error"), f(items(1)) result in Right("Find this one!"), and f(items(2)) result in launchTheNukes(); Right("Uh oh."), then the return should be Right("Find this one!") and no nukes should be launched.

It's sort of like what's happening in a for comprehension, where you could do:

for{
   res0 <- f(items(0))
   res1 <- f(items(1))
   res2 <- f(items(2))
} yield res2

Which would return either the first error or the final result - so I want that, but to handle an arbitrary list rather than hard-coded, and returning the first success, not the first error. (The answer I'm looking for might be two functions, one to swap the sides of an Either, and one to automatically chain foldLefts across a list... I think there's a single-step solution though.)

Code snippet for commented solution:

def tester(i : Int) : Either[String, Int] = {if (i % 2 == 0) Right(100 / (4 - i)) else Left(i.toString)}
(1 to 5).collectFirst(tester)

Solution

  • I'm assuming (from your mention of more complex monads such as State) that you're using the Cats library.

    You probably want one of the methods that come from Traverse

    For example, its sequence and traverse methods are two variations of the "I have a list of things, and a thing-to-monad function, and I want a monad of things". Since Either is a monad whose flatMap aborts early upon encountering a Left, you could .swap your Eithers so that the flatMap aborts early upon encountering a Right, and then .swap the result back at the end.

    def tester(i : Int): Either[String, Int] = /* from your question */
    val items = (1 to 5).toList
    
    items.traverse(tester(_).swap).swap // Right(50)
    
    val allLeft = List(Left("oh no"), Left("uh oh"))
    allLeft.traverse(_.swap).swap // Left(List("oh no", "uh oh"))