Eta-expansion competes with match type, contextual application
Compiler version
3.2.2
Minimized code
object BugReport:
trait Executor
trait Updatable[+A]
def run(task: Executor ?=> Unit): Unit = {}
def tupledFunction(a: Int, b: Int): Unit = {}
def tupledSequence(f: ((Updatable[Int], Updatable[Int])) => Unit): Unit = {}
type UpdatableMap[T <: Tuple] = T match
case EmptyTuple => EmptyTuple
case h *: t => Updatable[h] *: UpdatableMap[t]
def liftAsTupledInThreads[A <: Tuple](f: A => Unit)(using e: Executor): UpdatableMap[A] => Unit = ???
run {
tupledSequence(liftAsTupledInThreads(tupledFunction.tupled))
}
run {
val lifted = liftAsTupledInThreads(tupledFunction.tupled)
tupledSequence(lifted)
}
Output
BugReport.scala:16:20
Found: (e : (BugReport.Updatable[Int],
BugReport.Updatable[Int]
))
Required: BugReport.Executor
tupledSequence(liftAsTupledInThreads(tupledFunction.tupled))
Expectation
Two "run" sections are essentially the same, but the second compiles as expected, and the first fails.
The error message suggests that it swaps the argument of the returned function and the context bound.
Works with
def liftAsTupledInThreads[A <: Tuple](f: A => Unit): Executor ?=> UpdatableMap[A] => Unit = ???
I was hoping the rules about turning things into context functions would pop back in my head, but I'll get a coffee and re-read the docs.
The thing that troubles me most is Found: (e : (BugReport.Updatable[Int], BugReport.Updatable[Int])), where e was the name given to Executor. This looks really suspicious to me.
I wonder if it sees:
run { (task: Executor) ?=>
tupledSequence {
liftAsTupledInThreads(
tupledFunction.tupled: ((Int, Int) => Unit)
): ((Updatable[Int], Updatable[Int])) => Unit
// typed as
e => liftAsTupledInThreads(arg): ((Updatable[Int], Updatable[Int])) => Unit
}
}
because it's eta-expanded due to expected type, not applied as one might expect.
BugReport.run(
{
def $anonfun(using evidence$1: BugReport.Executor): Unit =
{
BugReport.tupledSequence(
{
val f$1: ((Int, Int)) => Unit =
{
def $anonfun(a: Int, b: Int): Unit = BugReport.tupledFunction(a, b)
closure($anonfun)
}.tupled
{
def $anonfun(using e: (BugReport.Updatable[Int], BugReport.Updatable[Int])): Unit =
{
BugReport.liftAsTupledInThreads[(Int, Int)](f$1)(using e)
()
}
closure($anonfun)
}
}
)
}
closure($anonfun)
}
)
I guess I would also expect the implicit application before eta expansion.
The thing is, if one gets rid of the tuples and the mapper, it compiles just nicely:
object BugReport:
trait Executor
trait Updatable[+A]
def run(task: Executor ?=> Unit): Unit = {}
def function(a: Int): Unit = {}
def normalSequence(f: Updatable[Int] => Unit): Unit = {}
def liftInThreads[A](f: A => Unit)(using e: Executor): Updatable[A] => Unit = ???
run {
normalSequence(liftInThreads(function))
}
so this is I don't know what, a conflict of stages maybe, but not me misunderstanding the rules.
Earlier, a timestamp made me think this was still 2024.
type UpdatableMap[_] = (Updatable[Int], Updatable[Int])
makes it work, so as noted in the previous comment, it's not just a competition between eta-expansion and implicit application, but match types.
It's the expected type that makes the difference, so this fails:
val lifted: ((Updatable[Int], Updatable[Int])) => Unit = liftAsTupledInThreads(tupledFunction.tupled)
The expansion looks "suspicious" just because the expected type determines the parameter type (but it keeps the name e):
{
val f$1: ((Int, Int)) => Unit =
(a: Int, b: Int) => BugReport.tupledFunction(a, b).tupled
(using e: (BugReport.Updatable[Int], BugReport.Updatable[Int])) =>
BugReport.liftAsTupledInThreads[(Int, Int)](f$1)(using e)
}
so that is normal.
The workaround noted above is
def liftAsTupledInThreads[A <: Tuple](f: A => Unit): Executor ?=> UpdatableMap[A] => Unit = _ => ()
Another workaround is to make it transparent inline
transparent inline
def liftAsTupledInThreads[A <: Tuple](f: A => Unit)(using e: Executor): UpdatableMap[A] => Unit = _ => ()
Maybe that is standard advice, if you want your match type to work under these constraints, make it inferred in typer using transparent inline. Is that because the match type is in the result type of the method?
For completeness,
object BugReport:
trait Executor
trait Updatable[+A]
def run(task: Executor ?=> Unit): Unit = ()
def tupledFunction(a: Int, b: Int): Unit = ()
def tupledSequence(f: ((Updatable[Int], Updatable[Int])) => Unit): Unit = ()
type UpdatableMap[T <: Tuple] = T match
case EmptyTuple => EmptyTuple
case h *: t => Updatable[h] *: UpdatableMap[t]
type xUpdatableMap[_] = (Updatable[Int], Updatable[Int])
transparent inline
def liftAsTupledInThreads[A <: Tuple](f: A => Unit)(using e: Executor): UpdatableMap[A] => Unit = _ => ()
def xliftAsTupledInThreads[A <: Tuple](f: A => Unit): Executor ?=> UpdatableMap[A] => Unit = _ => ()
run:
tupledSequence(liftAsTupledInThreads(tupledFunction.tupled)) // error
run:
val lifted = liftAsTupledInThreads(tupledFunction.tupled)
//val lifted: ((Updatable[Int], Updatable[Int])) => Unit = liftAsTupledInThreads(tupledFunction.tupled)
tupledSequence(lifted)
Notably, the pos test doesn't pass pickling test.
[info] Test dotty.tools.dotc.CompilationTests.pickling started
[ ] completed (0/1, 0 failed, 2s)Fatal compiler crash when compiling: tests/pos/i17210.scala:
pickling difference for object BugReport in tests/pos/i17210.scala, for details:
diff before-pickling.txt after-pickling.txt
to wit
147,148c147,148
< ) => <():scala.Unit>@tests/pos/i17210.scala<658..660>:BugReport.UpdatableMap[(scala.Int, scala.Int)] =>
< scala.Unit>@tests/pos/i17210.scala<653..660>
---
> ) => <():scala.Unit>@tests/pos/i17210.scala<658..660>:(BugReport.Updatable[scala.Int],
> BugReport.Updatable[scala.Int]) => scala.Unit>@tests/pos/i17210.scala<653..660>
150c150,151
< :BugReport.UpdatableMap[(scala.Int, scala.Int)] => scala.Unit>@tests/pos/i17210.scala<877..921>
---
> :(BugReport.Updatable[scala.Int], BugReport.Updatable[scala.Int]) => scala.Unit>@
> tests/pos/i17210.scala<877..921>
I don't know if that is a testing artifact.
(Edit: there is an exclusion list for tests which don't pass pickling, including because match types.)
(The explanation, that transparent inline is needed so that the match type is available for adaptation to the expected type, makes sense to me, but I don't know the details. Usually one hopes that the "order of operations" will fall out naturally and just work.)