Proposal: Inline varargs parameters

I would like to propose the addition of special treatment for inline varargs parameters for inline def.

Rather than creating a full sequence with the inputs, and then passing that sequence to the method. The method would receive a special kind of Iterator that would allow it to retrieve each element of the varargs parameter during compile time. This would require some form of inline while construct that would generate new code on each iteration.

This would allow for collection factories to be more efficient and delegate directly to builders.
This may also allow more advanced meta-programming capabilities in a more familiar environment without needing to go all the way down to macros. For example, this together with summonInline would be very useful for writing custom string interpolators.

6 Likes

Can you give an explicit example?

I think this is already possible? I know a lot of libraries that produce string interpolators rely on this:

//> using scala 3.5.0-RC2

import scala.quoted.*

inline def test(inline args: String*) = ${ testImpl('args) }

def testImpl(args: Expr[Seq[String]])(using Quotes): Expr[String] =
  val Varargs(sc) = args // match only literal collection at compile time

  if sc.size < 2 then '{ "not enough" }
  else '{ ${ sc(0) } + ${ sc(1) } }

and usage:

@main def hello = 
  println(test("a", "b")) // this compiles

  val x = List.fill(5)("a")
  test(x*) // this doesn't compile, as varags cannot be matched at compile time

I think the point was avoiding quotes and splices (which is good because then pipelining works wahoo)

2 Likes

Ah I see. Got it.

Using some pseudo code, I would imagine being able to write something like this.

// Definition:
object List:
  inline def apply[A](inline as: A*): List[A] =
      val builder = List.newBuilder[A]
      inline while (as.hasNext):
        builder.addOne(as.next())
      end while
      builder.result()
  end apply
end List

// Usage:
val data = List(1, 2, 3)

// Expands to:
val data = {
  val builder = List.newBuilder[A]
  builder.addOne(1)
  builder.addOne(2)
  builder.addOne(3)
  builder.result()
}

Which would save the allocation of an intermediate Seq to then be converted into a List.
Note, that AFAIK, the compiler already has a similar optimization in place. But, this would be a general mechanism that could be used for all collections and for users outside of the stdlib; like cats NonEmptyList.


Of course, there is also the case that the usage was something like: List.apply(existingSeq*) in that case this wouldn’t work.
I guess a simple solution would be to use an inline match:

object List:
  inline def apply[A](inline as: A*): List[A] =
    inline as match
      case as: CompileTimeIterator[A] =>
        val builder = List.newBuilder[A]
        inline while (as.hasNext):
          builder.addOne(as.next())
        end while
        builder.result()

      case as: Seq[A] =>
        List.from(as)
  end apply
end List

This would allow to make this change without breaking existing code.


Additionally, I would like if such CompileTimeIterator would have two extra methods:

  • size: this should be known.
  • reverse: this should be easy.
  • foreach: helper for simple cases, like the above:
object List:
  inline def apply[A](inline as: A*): List[A] =
    val builder = List.newBuilder[A]
    as.foreach(builder.addOne)
    builder.result()
  end apply
end List
1 Like