Scala - evaluate function calls sequentially until one return is returned

I have a few "obsolete" 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 DataX not found
  • return types for each method are different. There is a convert method that converts each DataX into a unified Data .
  • Endpoints are not Scala -ish

What is the best Scala approach to consistently evaluate these method calls until you get the right value?

In pseudo, I would do something like:

 val myData = maybeMyDataInEndpoint1 getOrElse maybeMyDataInEndpoint2 getOrElse maybeMyDataInEndpoint3 
+7
scala
source share
5 answers

I would use a simpler approach, although other answers use more complex language functions. Just use Option() to catch the null chain, the orElse chain. I accept convertX(d:DataX):Data methods for explicit conversion. Since this may not be found at all, we return Option

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

Perhaps you can raise these methods as high-order functions List and collectFirst , for example:

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

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

  • To handle optional values ​​(or null from the Java land), use Option .
  • To sequentially evaluate the list of methods, roll up on Seq functions.
  • To convert from one data type to another, use (1.) implicit conversions or (2.) regular functions depending on the situation and your preferences.

    • ( 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)) 
    • ( 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 
+5
source share

Here is one way to do it.

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

(2) Then use Stream to build chain from method calls. You must specify type inference that you want your stream to contain Data elements (and not DataX as returned by obsolete methods), so for each result of calling the inherited method, the corresponding implicit convert will be applied.

(3) Since Stream lazy and evaluates its tail β€œby name”, only the first method is still called. At this point, you can apply a lazy filter to skip null results.

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

(HACK) Unfortunately, scala type output (at the time of writing, v2.12.4) is not efficient enough to use the #:: stream methods if you don’t direct it to every step by the way. Using cons makes the conclusion happy but cumbersome. In addition, creating a stream using the vararg apply method of a companion object is also not an option, since scala does not support varargs by name. In my example below, I use a combination of the Stream and toLazyData . Stream - a common helper, builds streams from 0-arg functions. toLazyData is an implicit "by name" conversion designed to interact with the implicit convert functions that convert from DataX to Data .

Here is a demon demonstrating the idea in 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) } } 

Fingerprints:

 maybeMyDataInEndpoint1 maybeMyDataInEndpoint2 Data( Demo$DataB@16022d9d ) 

Here maybeMyDataInEndpoint1 returns null , and maybeMyDataInEndpoint2 needs to be called, DataB delivery, maybeMyDataInEndpoint3 will never be called, because we already have the result.

+3
source share

I think @ g.krastev's answer is great for your use case, and you should accept this. I just diverge a little on it to show how you can make the last step a little better with cats .

First, the template:

 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 = ai def convertB(b: DataB): Data = bi def convertC(c: DataC): Data = ci 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 such a way that you can copy / paste into the REPL and 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) 

In doing so, you can, for example, call the following to turn maybeMyDataInEndpoint1 into UUID => Option[A] :

 makeSafe(maybeMyDataInEndpoint1, convertA) 

The idea now is to turn your endpoints into a UUID => Option[A] list and add this list. Here is your list:

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

Now you can reset it manually, as @ g.krastev did:

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

If you're fine with the cats dependency, the notion of folding over a list of options is just a concrete example of using a common template ( Foldable and Monoid ):

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

There are other ways to do this even better, but in this context they may be redundant - I would probably declare a type class to turn any type into Data , for example to give makeSafe a cleaner signature type.

+2
source share

All Articles