Scala Programming Language
  1. Scala Programming Language
  2. SI-302

partial application of methods ignores call-by-name parameters

    Details

    • Type: Bug Bug
    • Status: Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: None
    • Fix Version/s: None
    • Labels:
      None

      Description

      Consider the following:

      object Test {
        def test(x: => Unit)(y: Unit): String = "done"
      
        def main(args: Array[String]) {
          val t = test(println("never seen"))_
          println(t())
        }
      }
      

      This produces:

      never seen
      done
      

      It should produce just:

      done
      

      scalac -Xprint:typer shows that the definition of t is compiled as:

      val t: (Unit) => String = {
        <synthetic> val eta$$0$$1: Unit = scala.this.Predef.println("never seen");
        ((eta$$1$$1: Unit) => Test.this.test(eta$$0$$1)(eta$$1$$1))
      };
      

      This eta-expansion incorrectly ignores that the first parameter of test is call-by-name.

        Issue Links

          Activity

          Hide
          Johannes Rudolph added a comment -

          I thought a bit about this and it seems clear that the current behavior is wrong because it violates the expectation that

          (test { println("Test" } _)()
          

          has the same result as

          test { println("Test" }()
          

          I.e. that you can either apply a function piece-wise partially or directly with all arguments and get the same result.

          I think the solution as given by Matt is pretty good: at eta-expansion by-name parameters are lifted to vals of a thunk value (of type () => T). However, this is not a valid type to pass as a by-name argument so the application has to change from x1 to x1.apply() as well. If you do this naively the compiler will then (in uncurry) create another redundant thunk out of this application. I therefore propose another change where an expression f.apply() with f being an instance of Function0 in by-name-argument position isn't thunked but instead the application is removed and f passed directly. This should be a safe optimization in all cases because the generated thunk never does more than simple thunking.

          The hard part of producing proper legalese for the scripture maybe isn't so hard at all because we have a precedent by now. This already is in "§6.6 Function Applications":

          The behavior of by-name parameters is preserved if the application is transformed into a block due to named or default arguments. In this case, the local value for that parameter has the form val y_i = () => e and the argument passed to the function is y_i ().

          We just have to amend the first sentence by "or eta-expansion". Problem solved.

          If we can reach a consensus that this is the right approach I can put together a pull request with an implementation proposal.

          Show
          Johannes Rudolph added a comment - I thought a bit about this and it seems clear that the current behavior is wrong because it violates the expectation that (test { println("Test" } _)() has the same result as test { println("Test" }() I.e. that you can either apply a function piece-wise partially or directly with all arguments and get the same result. I think the solution as given by Matt is pretty good: at eta-expansion by-name parameters are lifted to vals of a thunk value (of type () => T). However, this is not a valid type to pass as a by-name argument so the application has to change from x1 to x1.apply() as well. If you do this naively the compiler will then (in uncurry) create another redundant thunk out of this application. I therefore propose another change where an expression f.apply() with f being an instance of Function0 in by-name-argument position isn't thunked but instead the application is removed and f passed directly. This should be a safe optimization in all cases because the generated thunk never does more than simple thunking. The hard part of producing proper legalese for the scripture maybe isn't so hard at all because we have a precedent by now. This already is in "§6.6 Function Applications": The behavior of by-name parameters is preserved if the application is transformed into a block due to named or default arguments. In this case, the local value for that parameter has the form val y_i = () => e and the argument passed to the function is y_i (). We just have to amend the first sentence by "or eta-expansion". Problem solved. If we can reach a consensus that this is the right approach I can put together a pull request with an implementation proposal.
          Hide
          Johannes Rudolph added a comment -

          See also SI-1247 for the optimization I proposed.

          Show
          Johannes Rudolph added a comment - See also SI-1247 for the optimization I proposed.
          Hide
          Robert Gibson added a comment -

          Duplicate of SI-5610 I guess.

          Show
          Robert Gibson added a comment - Duplicate of SI-5610 I guess.
          Hide
          Matt Hellige added a comment -

          Considering that this bug is five years and 5300 bugs older, I'd think that would go the other way...

          Show
          Matt Hellige added a comment - Considering that this bug is five years and 5300 bugs older, I'd think that would go the other way...
          Hide
          Lukas Rytz added a comment -

          fixed together with SI-5610

          Show
          Lukas Rytz added a comment - fixed together with SI-5610

            People

            • Assignee:
              Lukas Rytz
              Reporter:
              Matt Hellige
              TracCC:
              Johannes Rudolph, Lachlan O'Dea, Seth Tisue
            • Votes:
              1 Vote for this issue
              Watchers:
              6 Start watching this issue

              Dates

              • Created:
                Updated:
                Resolved:

                Development