Saturday, January 6, 2018

Scala - evaluate function calls sequentially until one return

Leave a Comment

I have a few 'legacy' endpoints that can return the Data I'm looking for.

def mainCall(id): Data {      maybeMyDataInEndpoint1(id: UUID): DataA      maybeMyDataInEndpoint2(id: UUID): DataB      maybeMyDataInEndpoint3(id: UUID): DataC } 
  • null can be returned if no DataX found
  • return types for each method are different. There are a convert method that converting each DataX to unified Data.
  • The endpoints are not Scala-ish

What is the best Scala approach to evaluate those method calls sequentially until I have the value I need?

In pseudo I would do something like:

val myData = maybeMyDataInEndpoint1 getOrElse maybeMyDataInEndpoint2 getOrElse maybeMyDataInEndpoint3 

5 Answers

Answers 1

I'd use an easier approach, though the other Answers use more elaborate language features. Just use Option() to catch the null, chain with orElse. I'm assuming methods convertX(d:DataX):Data for explicit conversion. As it might not be found at all we return an Option

def mainCall(id: UUID): Option[Data] {   Option(maybeMyDataInEndpoint1(id)).map(convertA)   .orElse(Option(maybeMyDataInEndpoint2(id)).map(convertB))   .orElse(Option(maybeMyDataInEndpoint3(id)).map(convertC)) } 

Answers 2

Maybe You can lift these methods as high order functions of Lists and collectFirst, like:

  val fs = List(maybeMyDataInEndpoint1 _, maybeMyDataInEndpoint2 _, maybeMyDataInEndpoint3 _)    val f = (a: UUID) => fs.collectFirst {     case u  if u(a) != null => u(a)   }   r(myUUID) 

Answers 3

The best Scala approach IMHO is to do things in the most straightforward way.

  • To handle optional values (or nulls from Java land), use Option.
  • To sequentially evaluate a list of methods, fold over a Seq of functions.
  • To convert from one data type to another, use either (1.) implicit conversions or (2.) regular functions depending on the situation and your preference.

    1. (Edit) Assuming implicit conversions:

      def legacyEndpoint[A](endpoint: UUID => A)(implicit convert: A => Data) =   (id: UUID) => Option(endpoint(id)).map(convert)  val legacyEndpoints = Seq(   legacyEndpoint(maybeMyDataInEndpoint1),   legacyEndpoint(maybeMyDataInEndpoint2),   legacyEndpoint(maybeMyDataInEndpoint3) )  def mainCall(id: UUID): Option[Data] =   legacyEndpoints.foldLeft(Option.empty[Data])(_ orElse _(id)) 
    2. (Edit) Using explicit conversions:

      def legacyEndpoint[A](endpoint: UUID => A)(convert: A => Data) =   (id: UUID) => Option(endpoint(id)).map(convert)  val legacyEndpoints = Seq(   legacyEndpoint(maybeMyDataInEndpoint1)(fromDataA),   legacyEndpoint(maybeMyDataInEndpoint2)(fromDataB),   legacyEndpoint(maybeMyDataInEndpoint3)(fromDataC) )  ... // same as before 

Answers 4

Here is one way to do it.

(1) You can make your convert methods implicit (or wrap them into implicit wrappers) for convenience.

(2) Then use Stream to build chain from method calls. You should give type inference a hint that you want your stream to contain Data elements (not DataX as returned by legacy methods) so that appropriate implicit convert will be applied to each result of a legacy method call.

(3) Since Stream is lazy and evaluates its tail "by name" only first method gets called so far. At this point you can apply lazy filter to skip null results.

(4) Now you can actually evaluate chain, getting first non-null result with headOption

(HACK) Unfortunately, scala type inference (at the time of writing, v2.12.4) is not powerful enough to allow using #:: stream methods, unless you guide it every step of the way. Using cons makes inference happy but is cumbersome. Also, building stream using vararg apply method of companion object is not an option too, since scala does not support "by-name" varargs yet. In my example below I use combination of stream and toLazyData methods. stream is a generic helper, builds streams from 0-arg functions. toLazyData is an implicit "by-name" conversion designed to interplay with implicit convert functions that convert from DataX to Data.

Here is the demo that demonstrates the idea with more detail:

object Demo {    case class Data(value: String)   class DataA   class DataB   class DataC    def maybeMyDataInEndpoint1(id: String): DataA = {     println("maybeMyDataInEndpoint1")     null   }    def maybeMyDataInEndpoint2(id: String): DataB = {     println("maybeMyDataInEndpoint2")     new DataB   }    def maybeMyDataInEndpoint3(id: String): DataC = {     println("maybeMyDataInEndpoint3")     new DataC   }    implicit def convert(data: DataA): Data = if (data == null) null else Data(data.toString)   implicit def convert(data: DataB): Data = if (data == null) null else Data(data.toString)   implicit def convert(data: DataC): Data = if (data == null) null else Data(data.toString)    implicit def toLazyData[T](value: => T)(implicit convert: T => Data): (() => Data) = () => convert(value)    def stream[T](xs: (() => T)*): Stream[T] = {     xs.toStream.map(_())   }    def main (args: Array[String]) {      val chain = stream(       maybeMyDataInEndpoint1("1"),       maybeMyDataInEndpoint2("2"),       maybeMyDataInEndpoint3("3")     )      val result = chain.filter(_ != null).headOption.getOrElse(Data("default"))      println(result)    }  } 

This prints:

maybeMyDataInEndpoint1 maybeMyDataInEndpoint2 Data(Demo$DataB@16022d9d) 

Here maybeMyDataInEndpoint1 returns null and maybeMyDataInEndpoint2 needs to be invoked, delivering DataB, maybeMyDataInEndpoint3 never gets invoked since we already have the result.

Answers 5

I think @g.krastev's answer is perfectly good for your use case and you should accept that. I'm just expending a bit on it to show how you can make the last step slightly better with cats.

First, the boilerplate:

import java.util.UUID  final case class DataA(i: Int) final case class DataB(i: Int) final case class DataC(i: Int) type Data = Int  def convertA(a: DataA): Data = a.i def convertB(b: DataB): Data = b.i def convertC(c: DataC): Data = c.i  def maybeMyDataInEndpoint1(id: UUID): DataA = DataA(1) def maybeMyDataInEndpoint2(id: UUID): DataB = DataB(2) def maybeMyDataInEndpoint3(id: UUID): DataC = DataC(3) 

This is basically what you have, in a way that you can copy/paste in the REPL and have compile.

Now, let's first declare a way to turn each of your endpoints into something safe and unified:

def makeSafe[A, B](evaluate: UUID ⇒ A, f: A ⇒ B): UUID ⇒ Option[B] =    id ⇒ Option(evaluate(id)).map(f) 

With this in place, you can, for example, call the following to turn maybeMyDataInEndpoint1 into a UUID => Option[A]:

makeSafe(maybeMyDataInEndpoint1, convertA) 

The idea is now to turn your endpoints into a list of UUID => Option[A] and fold over that list. Here's your list:

val endpoints = List(   makeSafe(maybeMyDataInEndpoint1, convertA),   makeSafe(maybeMyDataInEndpoint2, convertB),   makeSafe(maybeMyDataInEndpoint3, convertC) ) 

You can now fold on it manually, which is what @g.krastev did:

def mainCall(id: UUID): Option[Data] =    endpoints.foldLeft(None: Option[Data])(_ orElse _(id)) 

If you're fine with a cats dependency, the notion of folding over a list of options is just a concrete use case of a common pattern (the interaction of Foldable and Monoid):

import cats._ import cats.implicits._  def mainCall(id: UUID): Option[Data] = endpoints.foldMap(_(id)) 

There are other ways to make this nicer still, but they might be overkill in this context - I'd probably declare a type class to turn any type into a Data, say, to give makeSafe a cleaner type signature.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment