An Interesting Context-Function Shorthand

By accident, I discovered that the following compiles:

type ContextFunc = Array[Int] ?=> Unit

def test(f: ContextFunc): Unit =
  val a = Array(1,2,3)
  f(using a)
  a.foreach(println)

def updateArr(using a: Array[Int]): Unit =
  a(0) += 99

// I can't name this method `main`!?
@main def runApp: Unit =
  test(1+1, updateArr, println("hello world"))

It prints

hello world
100
2
3

Here’s what I think happens. The arguments to test are automatically tupled. This is possible because ContextFunc, after application of the given Array[Int], is actually the by-name => Unit. So passing a tuple T to test expands the parameter to T => Unit. Please let me know if I have this completely backwards.

You can use this to make concise builders. From the docs on context-functions, instead of

  table {
     row {
        cell("top left")
        cell("top right")
     }
     row {
        cell("bottom left")
        cell("bottom right")
     }
  }

The following is equivalent & shorter:

table {
  row(cell("top left"), cell("top right"))
  row(cell("bottom left"), cell("bottom right"))
}
2 Likes

That’s very weird. I would want the type system to catch this as a mistake, not allow it as a weird quirk that allows concise builders without specifically building them to be concise.

9 Likes

On one hand, I agree it is weird.

But, you can reproduce the same thing using a by-name parameter (tested Scala 2.11 - 3-M3):

def test(f: => Unit): Unit =
  println(f)

@main def runApp: Unit =
  test(1+1, println("hello world"))

This prints:

hello world
()

So despite its weirdness you could use it make a clean html dsl, since html elements have inline attributes, and children within blocks.

How is that better than this:

table {
  row{ cell("top left"); cell("top right") }
  row{ cell("bottom left"); cell("bottom right") }
}

I’d prefer to retain consistency, and the ability of eliding separators (here semicolons) when putting things on multiple lines.

4 Likes

To get more detailed info, it helps passing -Xprint:typer as an option to the compiler. That gives:

result of test.scala after typer:
package <empty> {
  final lazy module val test$package: test$package$ = new test$package$()
  final module class test$package$() extends Object() { 
    this: test$package.type =>
    type ContextFunc = ContextFunction1[Array[Int], Unit]
    def test(f: ContextFunc): Unit = 
      {
        val a: Array[Int] = Array.apply(1, [2,3 : Int]*)
        f.apply(using a)
        intArrayOps(a).foreach[Unit](
          {
            def $anonfun(x: Any): Unit = println(x)
            closure($anonfun)
          }
        )
      }
    def updateArr(using a: Array[Int]): Unit = a.update(0, a.apply(0).+(99))
    @main() def runApp: Unit = 
      test(
        {
          def $anonfun(using evidence$1: Array[Int]): Unit = 
            {
              Tuple3.apply[Int, Unit, Unit](1.+(1), updateArr(evidence$1), 
                println("hello world")
              )
              ()
            }
          closure($anonfun)
        }
      )
  }
  final class runApp() extends Object() {
    <static> def main(args: Array[String]): Unit = 
      try test$package.runApp catch 
        {
          case error @ _:scala.util.CommandLineParser.ParseError => 
            scala.util.CommandLineParser.showError(error)
        }
  }
}

So what happens here is the following:

The three arguments 1+1, updateArr, println("hello world") are indeed auto-tupled, so you get

    (1+1, updateArr, println("hello world"))

That is typed under a lambda since the expected result type is a context function:

   (_: Array[Int]) ?=> (1+1, updateArr, println("hello world"))

Now unit discarding kicks in, yielding

   (_: Array[Int]) ?=> { (1+1, updateArr, println("hello world")); () }

Unit discarding is not essential. The same works also if ContextFunc is defined like this:

type ContextFunc = Array[Int] ?=> Any

What to do about it? I believe the culprit here is clearly auto-tupling. If I compile with -language:noAutopTupling I get:

sc test.scala -language:noAutoTupling
-- Error: test.scala:13:12 -----------------------------------------------------
13 |  test(1+1, updateArr, println("hello world"))
   |            ^^^^^^^^^
   |            too many arguments for method test: (f: ContextFunc): Unit
1 error found

We’d like to get rid of auto-tupling. I have tried really hard to do so and have spent considerable time on it. But in the end, the breakage wrt 2.13 was too big a risk. All we could do in 3.0 was remove some of the more egregious pitfalls of auto-tupling. So we could not do all of it yet, but hopefully it will come in the future.

And, please don’t turn this into a feature by exploiting this in clever ways. It will likely cause a lot of problems later when we remove auto-tupling.

6 Likes

We’d like to get rid of auto-tupling

That is good to know, thanks!

Although now that I’m the devil’s advocate, I think there still could be some utility in one-liner builders. What do you think about varargs?

type ContextFunc = Int ?=> Unit

def test(cf: ContextFunc*): Unit =
  for f <- cf do f(using 1)

def p1(using i: Int): Unit =
  println(i)

def p2(using i: Int): Unit =
  println(i + 1)


@main def runApp: Unit =
  test {
    p1
    p2
  }
  println()
  test(p1, p2)

Both work with "-language:noAutoTupling"

1
2

1
2

Yes, varargs is fine, Still looks a bit weird that you pass arguments that evaluate to Unit after a performing a side effect. But if it’s well encapsulated, it’s OK.

I still don’t understand how updateArr get bound to f (f=updateArr)?