Question

I have a sealed trait which is implemented by 3 objects

sealed trait MyTrait {
...
}
object A extends MyTrait { ... }
object B extends MyTrait { ... }
object C extends MyTrait { ... }

I'm using Scalaz's validation mechanism wherein the apply methods of the objects A, B and C return a validated type. the Objects A, B and C does contain some logic and I want to apply this logic sequentially, i.e., I want to first apply A and check what the result of A is and based on it, decide if I want to call B or just return the validated result. I want to repeat this until I hit C after which I just return whatever I get as a result of calling C.

Currently I have a static approach where I first call A, pass the result of A to a utility method and check for the result and then call B.

  def apply(request: Request): Validated[Result] = {
    val vResultA = run(request, A)
    val vResultB = if (isResultOk(vResultA)) run(request, B) else vResultA
    if (isResultOk(vResultB)) run(request, C) else vResultB
  }

Is there a better way to do this? Any suggestions or any patterns that I can apply?

Was it helpful?

Solution

We will define succeeded results = results that are OK, and failed results = results that are not OK.

First, A, B, and C are all objects extending MyTrait. Therefore, they can be grouped into an Array or a List of MyTrait.

val objects = Array(A, B, C) /* You can use List instead if you want. */

Then the type of objects is Array[MyTrait].

Next, we have to iterate on this Array.
However, just calling map on this Array continues mapping even if the previous isResultOk() is false.
Therefore, we will use Stream instead of Array.

Let's see how using Stream can stop calling map if some condition is satisfied.

Array(1, 2, 3, 4, 5).map(i => {
    println(i)
    i + 100
  }).takeWhile(_ <= 103).foreach(println(_))

The output of the above code will be:

1
2
3
4
5
101
102
103

So, map() ends, and then takeWhile() ends -- takeWhile() does not affect calling map().
However, if we do the same operations on the Stream,

Array(1, 2, 3, 4, 5).toStream.map(i => {
    println(i)
    i + 100
  }).takeWhile(_ <= 103).foreach(println(_))

The output will be:

1
101
2
102
3
103
4

So the calling will be map() -> takeWhile() -> foreach() -> map() -> takeWhile() -> ...
At the end, 4 is printed, and 4 + 100 = 104 > 103 will be cut in takeWhile().
The following elements will be not accessed further.

So, do we have to use takeWhile?

objects.toStream.map(run(request, _)).takeWhile(isResultOk(_))

This will get rid of failed results, even though we need the first failed result if failure occured.
(i.e. This will make a problem if there is any result that is not OK.)

How about the opposite function dropWhile()?

objects.toStream.map(run(request, _)).dropWhile(isResultOk(_))

This will get rid of all succeeded results, even though all results are succeeded.
(i.e. This will make a problem if all results are OK.)

So, we will use span().
c.span(p) = (c.takeWhile(p), c.dropWhile(p))
We will test if there are results that are not OK.
If there is a result that is not OK, then we will return the first such result.
Otherwise, we will return the last result that is OK.

val (succ, fail) = objects.toStream.map(run(request, _)).span(isResultOk(_))
fail.headOption.getOrElse(succ.last)

fail.headOption will return Some(fail's first element) if fail is not empty, otherwise None.

In summary,

val objects = Array(A, B, C)

def apply(request: Request): Validated[Result] = {
  val (succ, fail) = objects.toStream.map(run(request, _)).span(isResultOk(_))
  fail.headOption.getOrElse(succ.last)
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top