'Scala collectFirst with function returning Option[U]

I've had this situation occur a number of times in the library I'm writing, and I'm not particularly satisfied with the solutions I've come up with so far.

Let's say that I have an expensive function f that takes an item of type T and returns a value of type Option[U]. Now, suppose I have a collection of type T and I want to retrieve the first non-None value returned by f when performed across the elements of T without evaluating f for all elements of T if the value has already been found.

The only way I've come up with to do this is to wrap F into an Extractor Object, and use it with scala's collectFirst method.

For example:

object FMatch { def unapply(t : T) = f(t) }

collection.collectFirst{ case FMatch(result) => result }

This seems a little inelegant, and I'm not certain whether f is evaluated only once or twice for each result (I haven't tested this to find out yet). It seems like it would be useful to have a version of collectFirst that takes a parameter of type T => Option[U] instead of a PartialFunction1[T].

Is there a more elegant way to do this that I'm missing?



Solution 1:[1]

Use this:

collection.toIterator.map(f).find(_.isDefined)

Solution 2:[2]

Use a view over the collection, to make it lazy and defer invocation of that function until the last possible moment (e.g. it won't be called at all for elements beyond the first match):

xs.view map {f(_)} collectFirst {case Some(x) => x}

or

xs.view map {f(_)} find {_.isDefined}

or in the point-free style, as per Alexey's response:

xs.view map {f} find {_.isDefined}

That should hopefully give you a couple of alternative ways to think about the problem more generally :)

Solution 3:[3]

@annotation.tailrec 
def first[A, B](as: Traversable[A], f: A => Option[B]): Option[B] = 
  if (as.isEmpty) None 
  else f(as.head) match {
    case s @ Some(_) => s
    case _ => first(as.tail, f)
  }

Solution 4:[4]

Give a partial functions to collectFirst

collectionT
  .collectFirst { t => 
    f(t) match { 
      case Some(u) => u 
    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Somnath Muluk
Solution 2
Solution 3 Jed Wesley-Smith
Solution 4 Alexey Ki