Home > Software design >  Why does the compiler infer incorrectly return type for Future.sequence declaration in function?
Why does the compiler infer incorrectly return type for Future.sequence declaration in function?

Time:08-20

In scala2 or 3 this function compiles:

def returnsFutureOfFutureButCompiles(): Future[Unit] = {  
  for {
    _ <- Future.unit
  } yield Future.sequence(Seq(Future.unit)) // Returns Future[Future[Seq[Unit]]]
}

but this one does not:

def returnsFutureOfFutureButDoesNotCompile(): Future[Unit] = {
  val a = for {
    _ <- Future.unit
  } yield Future.sequence(Seq(Future.unit))
  a // Error: Required: Future[Unit], Found: Future[Future[Seq[Unit]]]
}

I am aware of for-comprehension syntaxic sugar, differences between map and flatMap and how monads work.

The compiler should warns for both functions

I don't understand these different behaviours since the functions are almost doing the same job.
Maybe it's related to type inference limitations but then, why?

Also, if I declare these functions:

def a(): Future[Int] = {
  for {
    _ <- Future.successful(1)
  } yield Future.successful(2) // Error: Required: Int, Found: Future[Int]
}

def b(): Future[Int] = {
  val value = for {
    _ <- Future.successful(1)
  } yield Future.successful(2)
  value // Error: Required: Future[Int], Found: Future[Future[Int]]
}

then the compiler warns correctly, as expected, about the two functions.

So, I think the problem is maybe related to Future.sequence?

CodePudding user response:

It's related to the fact that you used specifically Unit.

As you know

val a = for {
  _ <- Future.unit
} yield Future.sequence(Seq(Future.unit))

compiles to:

Future.unit.map { _ =>
  Future.sequence(Seq(Future.unit))
}

It should infer to Future[Future[Seq[Unit]]]. But when you told it that the result should be Future[Unit] compiler started working backward, from the type it should get, to the type it gets.

Therefore it infered that you wanted

Future.unit.map[Unit] { _ =>
  Future.sequence(Seq(Future.unit))
}

which is correct because when the type returned it Unit it can tread the code as if you wrote:

Future.unit.map[Unit] { _ =>
  Future.sequence(Seq(Future.unit))
  ()
}

However "dropping non-Unit value" can happen only if whole inference happen at once. When you do:

val a = for {
  _ <- Future.unit
} yield Future.sequence(Seq(Future.unit))
a

You have two statements/expressions, so a single inference cannot work backward. So know knowing that val a should be constrained to the type Future[Unit] it infer it to Future[Future[Seq[Unit]]]. The next, separate step, tries to match it against Future[Unit] and fails, because it is too late now to change the type of a previous statement and modify the body of map to drop the returned value.

In Scala 2 you could use -Wvalue-discard or -Ywarn-value-discard to force you to manually insert that () if you want to discard non-Unit value in body that should return Unit. Scala 3 is still waiting to obtain such warning flag.

CodePudding user response:

Maybe it's related to type inference limitations but then, why?

Yes, it is related to the type inference limitation. Your first code is equivalent to

def returnsFutureOfFutureButCompiles(): Future[Unit] = {  
  Future.unit.map(x => {
    Future.sequence(Seq(Future.unit)) 
  })
}

The whole body is just one statement/expression. The complier has to figure out the type of the inner function (lambda). Argument type is not explicitly specified, but it is from the parameters (it is Unit). What about return type? The expression is of the type Future.sequence(Seq(Future.unit)). But we also know that the whole expression should be of the type Future[Unit]. So the actual type the inner function (lambda) should return should also be Unit. The compiler just runs the computation inside the body and discards this. This is similar to the regular function where some non-unit expression may be compatible with the Unit return type. Compare it with the following:

def echo(x: Int): Int = {
  println(x)
  x
}

def useEcho(): Unit = {
  echo(3)
}

In the useEcho function the result of the expression (3) is "ignored" and converted into the unit.

Things change with the local variable. The second code is equivalent to

def returnsFutureOfFutureButDoesNotCompile(): Future[Unit] = {
  val a = Future.unit.map(x => {
    yield Future.sequence(Seq(Future.unit))
  })
  a 
}

Type inference boundaries are limited to one statement only. When complier builds types for the declaration of val a there is no clue what the return type of the inner lambda could be. So it makes the best guess and takes the type of the last lambda's expression as the return type. Unfortunately, this later leads to an error.

Your code samples with int does not compile. The reason should be clear now:

def a(): Future[Int] = {
  Future.successful(1).map(x => {
    Future.successful(2) // Error: Required: Int, Found: Future[Int]
  })
}

From the context we know the result should be Int, not Future. And the last example is completely similar to the returnsFutureOfFutureButDoesNotCompile.

The special bit here is the rule that allows to "discard" any value in the place where unit is required. You can even explicitly convert any value to unit like (5: Unit). And I think technically it could be implicit conversion as implicit conversions do apply to make the function match the code. Consider the following:

implicit def str2Int(x: String): Int = x.length

def dontDoThis(): Future[Int] =
  for {
    _ <- Future.successful(5)
  } yield "test"

This code compiles and works. This is because after for comprehension is converted to a call to map, the expected for the anonymous function becomes clear and compiler could apply the implicit converion. So the similar thing happens with the Unit type.

You can also help the compiler to provide proper conversion in you second example by giving some type hint like:

def returnsFutureAndCompiles(): Future[Unit] = {
  val a: Future[Unit] = for {
    _ <- Future.unit
  } yield Future.sequence(Seq(Future.unit))
  a
}

Now the expected type from the map function (i.e. yield's body) is known and compiler could apply necessary conversion to the Unit type.

  • Related