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.