scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

Eta-expansion competes with match type, contextual application

Open mbuzdalov opened this issue 2 years ago • 6 comments

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.

mbuzdalov avatar Apr 05 '23 08:04 mbuzdalov

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.

som-snytt avatar Apr 06 '23 16:04 som-snytt

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.

mbuzdalov avatar Apr 06 '23 16:04 mbuzdalov

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.

som-snytt avatar Apr 06 '23 17:04 som-snytt

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.

mbuzdalov avatar Apr 06 '23 17:04 mbuzdalov

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)

som-snytt avatar May 27 '25 21:05 som-snytt

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.)

som-snytt avatar May 27 '25 21:05 som-snytt