3/n - How do I in FP... trying several input for the same function

In the previous entry of this series, I wrote about error recovery in a functional way. But sometimes, we want to try several inputs until we retrieve (optionally) a result. This is the case if you have a function which compute a location from a string (think geo-search), and which optionally returns a result, if a place matched. We may try this function on several input strings and only care in the first result. In this article, we will see how to do this in a functional way.

Problem setup

Given a function def computeLocation(str: String): Option[Location] which tries to infer a location from a string and a list of input strings sorted by order of relevance (meaning that the first element may result in a more interesting location than the second and so on), let's retrieve the location (optionally). To sum it up:

// The computeLocation function:
def computeLocation(s: String): Option[Location] = // the implementation

// this call may generate a long List
val inputs: List[String] = createInputsFromForm(aForm)

How implement val result: Option[Location] = ??? ?

From scratch

The first solution is to call the computeLocation function for every string in the input list:

inputs.flatMap(computeLocation).headOption

This solution is the most simple ever. Let's break it into pieces to analyze it.
Let's start from the end. As we care only about the first meaningful result (which may not exist), we call headOption.
Now, back to the beginning of the line, we are calling flatMap on a List. The proper signature of flatMap in Scala standard library is slightly different from the one we are used to:

def flatMap[B](f: A => IterableOnce[B]): List[B] = //...

In the standard library, flatMap expects a function producing an IterableOnce. Fortunately Option implements this trait. The definition commonly used for flatMap is more like this one:

// Given that F is a type with one parameter
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] 

// For List this results in:
def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B]

In the standard library, the trick with IterableOnce spares us from writing something like:

inputs
    .flatMap{ s => 
        computeLocation(s).map(l => List(l)).getOrElse(Nil)
    }
    .headOption

In this code, we are manually transforming an Option into a List.

What's wrong with this implementation ? If you provide a dummy implementation for both computeLocation and createInputsFromForm, you will see that computeLocation is called for every single item in the list. If we consider that computing the location is a costly operation, it is sad to waste resource computing, while we only care about the first meaningful answer. How to make the flatMap sequence stop when we hit a result or the end of the list ?

Recursion by hand

If we break our problem in terms of imperative programming, we will easily come up with a while statement (in pseudo code):

while no result and list has elements left 
  result = computeLocation with next element in the list
done

Let's encode this with recursion:

@scala.annotation.tailrec
def recur(next: List[String]): Option[Location] = {
  next match {
    case Nil => None
    case head :: tail =>
      val r = computeLocation(head)
      if (r.isDefined) r else recur(tail)
  }
}
recur(inputs)

What we do is just manually traverse the list recursively until computeLocation gives a meaningful result. This is strictly the same as the imperative while version, but in FP, we like to look further. We could for instance abstract over the computeLocation and take as an extra parameter the function returning an Option:

@scala.annotation.tailrec
def recur(next: List[String],
          f: String => Option[Location]): Option[Location] = {
  next match {
    case Nil => None
    case head :: tail =>
      val r = f(head)
      if (r.isDefined) r else recur(tail, f)
  }
}
recur(inputs, computeLocation)

Looking at this code, do we really have to deal with String and Location ? We do not use any of the properties of these types in recur. Let's make them parameters.

@scala.annotation.tailrec
def recur[A, B](next: List[A], f: A => Option[B]): Option[B] = {
  next match {
    case Nil => None
    case head :: tail =>
      val r = f(head)
      if (r.isDefined) r else recur(tail, f)
  }
}
recur(inputs, computeLocation)

Following this reasoning we could go even further and add some mechanism to also deal with something else than Option and List. In fact, cats has such functions to hide recursion from you. It is called tailRecM but as it deserves an entire post for itself, I won't talk about it here.

Back with the Scala standard library...

In the standard library, we can also find the collectFirst function which, well do pretty much what we need !

inputs.collectFirst { str =>
  computeLocation(str) match {
    case Some(r) => r
  }
}

Well, the careful reader would have noticed that the function we pass to collectFirst is not total: it does not produce an output for every input.

As computeLocation is total, then it is a bit sad to write a non-total version of it... The standard library also provides a function to transform a total function returning Option to a partial one: def unlift[T, R](f: T => Option[R]): PartialFunction[T, R]. So we can now write:

inputs.collectFirst(Function.unlift(computeLocation))

If you are using cats, you will find collectFirstSome which will make this possible:

inputs.collectFistSome(computeLocation)

What if computeLocation perform other effects ?

Let's change the problem a bit now. What if the computeLocation function performs some network calls, or read a database to find a result ? These things are known to fail at one point or another. How to deal with that ?

Without adding too much complexity, let's consider that computeLocation has a more expressive output: Either[Throwable, Option[Location]]. This type means that computeLocation may fail with a Throwable or give a result if it was able to compute one (the absence of result being completely normal). The complete signature is now:

def computeLocation(rawStr: String): Either[Throwable, Option[Location]]

As you can see, the Option result is now boxed into Either. This makes both collectFirst and collectFirstSome impossible to use, as the type do not match now. In FP, this kind of boxing appears frequently, so that authors of functional libraries like cats already wrote functions to deal with this. In cats, you will often see functions whose name ends with a capital M. In our case, it is easy to find collectFirstSomeM which has this signature:

def collectFirstSomeM[G[_], B](f: A => G[Option[B]])
        (implicit F: Foldable[F], G: Monad[G]): G[Option[B]]

As you can see, this definition is abstract. Note that this function is part of a class parametrized on F, where F is itself a type with a "hole" : F[_]. In our example, this F will be Either[Throwable,_] (ie Either with only one type parameter applied). To make it easier, here is a version (pseudo scala) where abstract types are replaced with the one we use in our example (without the implicit part):

def collectFirstSomeM[Either[Throwable,_], Location](f: String => IO[Option[Location]]): Either[Throwable,Option[Location]]

Look how computeLocation has exactly the expected signature for the f parameter ! What about the implicits parameters now ? In this case, we can read the definition as: "the collectFirstSomeM method is available if and only if for the F type we can implement the trait Foldable and for the G type the Monad trait". In our case, with List and Either it is already done for us in cats so we can do:

import cats.implicits._
val inputs: List[String] = computeInputs(param)
val result: Either[Throwable, Option[Location]] = inputs.collectFirstSomeM(computeLocation)

Yet, if you write a simple sample for this, like :


def computeLocation(rawStr: String): Either[Throwable, Option[Location]] = {
    println(s"Computing with $rawStr") // to see something
    // Fake implementation
    if (rawStr.length == 1) Left(new IllegalStateException("wrong state"))
    else if (rawStr.length % 4 == 0) Right(Some(Location(2.0, 4.0, "Plop")))
    else Right(None)
}

val inputs = List("a", "impair?", "plop", "other")

import cats.implicits._
println(
  inputs.collectFirstSomeM(computeLocation)
)

You will notice that the output will be:

Computing with a
Left(java.lang.IllegalStateException: wrong state)

The execution is stopping as soon as a Left is returned ! This is perfectly normal as Either is a fail fast structure. And the requirement for Monad should have caught our attention: Monad express sequence, the next operation using the result of the previous one as input parameter.
So, as the caller of computeLocation in our situation, we have two options:

  • consider a Left as a blocker, and indeed, keep it,
  • try to recover from it by composing computeLocation with a function handling the situation with care.

Here is an example of the second choice, being ignoring errors (you probably don't want to do that, and perform a selective recovery, composing with things we saw earlier):

inputs.collectFirstSomeM(
  s => computeLocation(s).recoverWith(_ => Either.right(None))
)

Conclusion

In this article, we saw how to deal with something simple: calling a function for elements in a list and keeping the first meaningful result (if any). Even if it was simple, what I would like to highlight is how this solution is composable. Every type in the design models an intent:

  • Option models that the result may be irrelevant or present
  • Either models that a computation may fail
  • Foldable express that we want to build a single value from an initial structure (in our case, we fold the list to a single result)
  • Monad express that we compute in sequence: indeed, there is no parallelism involved in our solution, we try the input strings one after the other.

Yet, the work here is not over. Next time, we will see how we can adapt this example to make it less "sequential".