Skip to content

Eta-expansion competes with match type, contextual application #17210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
mbuzdalov opened this issue Apr 5, 2023 · 6 comments
Open

Eta-expansion competes with match type, contextual application #17210

mbuzdalov opened this issue Apr 5, 2023 · 6 comments
Labels
area:implicits related to implicits itype:bug

Comments

@mbuzdalov
Copy link

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 mbuzdalov added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Apr 5, 2023
@mbuzdalov mbuzdalov changed the title Compiler swaps context bound and the function argument?? Compiler swaps context bound and function argument?? Apr 5, 2023
@mbuzdalov mbuzdalov changed the title Compiler swaps context bound and function argument?? Compiler swaps context function arguments?? Apr 5, 2023
@som-snytt
Copy link
Contributor

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.

@mbuzdalov
Copy link
Author

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.

@som-snytt
Copy link
Contributor

som-snytt commented Apr 6, 2023

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.

@mbuzdalov
Copy link
Author

mbuzdalov commented Apr 6, 2023

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.

@anatoliykmetyuk anatoliykmetyuk added area:implicits related to implicits and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Apr 17, 2023
@som-snytt
Copy link
Contributor

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
Copy link
Contributor

som-snytt commented May 27, 2025

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 som-snytt changed the title Compiler swaps context function arguments?? Eta-expansion competes with match type, contextual application May 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:implicits related to implicits itype:bug
Projects
None yet
Development

No branches or pull requests

3 participants