Compose stateless futures with side-effecting futures in Scala

148 Views Asked by At

While composing futures with a for-yield structure, some with side effects, some without, I introduced a race condition because a future depending on a side effect did not take the result of that side effecting future as an argument.

In short:

future b reads a value that is changed by a side effect from future a, but future a does not explicitly depend on the result of future b and could therefore happen before b finishes reading.

To solve the problem, my colleague introduced a dummy function taking as an argument the result of b and simply throwing it away. This was done to make the dependency explicit.

The actual code is here:

  val futureConnection:Future[(Either[String, (Connection)],Boolean)] =
    for {
      scannerUser <- scanner.orFail("Scanning user not found")
      scannedUser <- futureScannedUser.orFail("Scanned user not found")
      existsInAnyDirection <- connections.existsInAnyDirection(scannerUser, scannedUser)
      connection <- connections.createConnection(scannerUser, scannedUser, form.magicWord, existsInAnyDirection)
    } yield {
      (connection, existsInAnyDirection)
    }

In this case, future b is

connections.existsInAnyDirection(scannerUser, scannedUser)

and future a with the dummy parameter is

connections.createConnection(scannerUser, scannedUser, form.magicWord, existsInAnyDirection)

Notice that the parameter existsInAnyDirection is never used inside createConnection. This effectively creates the dependency graph that createConnection cannot be initiated before existsInAnyDirection is completed.

Now for the question:

Is there a more sane way to make the dependency explicit?


Bonus Info

My own digging tells me, that the Scala Futures simply don't handle side effects very well. The methods on the Future trait that deal with side effects return Unit, whereas there could very well be results to read from a side effecting operation, i.e. error codes, generated ID's, any other meta info, really.

1

There are 1 best solutions below

0
On

Future is handling side effect of postponed computation like A => Future[B].

You tried to mix few different side effects but composed only one of them Future[_].

Try to choose second container, this can be Product or State, depends on your side effect and think in way of composing of side-effects (may be you will need modand transformers). And after your code can looks like (simplest cases):

for {
  scannerUser <- scanner.orFail("Scanning ...")
  (scannedUser, magicWord) <- futureScannedUser.orFail("Scanned ...")      
  connection <- connections.createConnection(scannerUser, scannedUser, magicWord)
} yield {
  (connection, existsInAnyDirection)
}

// OR

for {
  (scannerUser, state) <- scanner.orFail("Scanning ...")
  (scannedUser, nextState) <- futureScannedUser(state).orFail("Scanned ...")      
  connection <- connections.createConnection(scannerUser, scannedUser, nextState)
} yield {
  (connection, existsInAnyDirection)
}