SIP: Curried varargs

But with curried varargs I would imagine we would just need to write something like

class StringContext(s: String*, args: Any*) extends AnyVal {
      def applyNext[T](next: T)(given Show[T]): StringContext = StringContext(s, (args :+ next.show): _*)
      def applyEnd: String = this.s(args)
    }

or probably as extension methods.
Where each applyNext knows the type of the element and can find the necessary implicit.

I will try it out and have a complete example. The trick will be to use a pattern arg match { case '{ $arg: $t } => to recover the type of the argument. Iā€™ll have a look at the details.

Yes, with curried varargs.

Weā€™re currently without them, and have been told to do what we need to with current metaprogramming capabilities (which means in this case, mostly macros), or implement curried varargs using the same.

My primary usecase for curried varargs is exactly what youā€™ve proposed (Iā€™ve included in a previous comment an extremely similar sample implementation using curried varargs). Itā€™s also considerably simpler than what weā€™d need to do to implement curried varargs with a macro, so it provides a useful canary.

Thatā€™s an interesting bit of syntax, I had to go back through the docs to see if Iā€™d missed that. I could only find examples where that was part of the match, not a value bound by the match. If it works, that would be awesome.

Something needs to be done about the lack of documentation for this, so much is missing that right now macros seem very much like something reserved for the High Priesthood.

Thanks for giving it a shot, good luck :+1:

This seems to do the trick.

import scala.quoted._
import scala.quoted.matching._

inline def (sc: StringContext) showMe(args: =>Any*): String = ${ showMeExpr('sc, 'args) }

private def showMeExpr(sc: Expr[StringContext], argsExpr: Expr[Seq[Any]])(given qctx: QuoteContext): Expr[String] = {
  argsExpr match {
    case ExprSeq(argExprs) =>
      val argShowedExprs = argExprs.map {
        case '{ $arg: $tp } =>
          val showTp = '[Show[$tp]]
          searchImplicitExpr(given showTp, summon[QuoteContext]) match {
            case Some(showExpr) => '{ $showExpr.show($arg) }
            case None => qctx.error(s"could not find implicit for ${showTp.show}", arg); '{???}
          }
      }
      val newArgsExpr = Expr.ofSeq(argShowedExprs)
      '{ $sc.s($newArgsExpr: _*) }
    case _ =>
      // `new StringContext(...).showMeExpr(args: _*)` not an explicit `showMeExpr"..."`
      qctx.error(s"Args must be explicit", arg)
      '{???}
  }
}

trait Show[-T] {
  def show(x: T): String
}

given Show[Int] = x => s"Int($x)"
given Show[String] = x => s"Str($x)"
object Test {
  def main(args: Array[String]): Unit = {
    println(showMe"${1: Int} ${"abc": String}")
    println(showMe"${1} ${"abc"}")
    println(showMe"${1} ${println("xyz"); "xyz"}")
  }
}

which prints

Int(1) Str(abc)
Int(1) Str(abc)
xyz
Int(1) Str(xyz)
1 Like

Thanks!

Looks like underlyingArgument turned out to be a red herring. I was playing around with what you suggested, and it ended up very close structurally to what youā€™ve got but throws this error during compilation:

[error] 10 |    assertEquals(show"${1}", "")
[error]    |                 ^^^^^^^^^^
[error]    |Exception occurred while executing macro expansion.
[error]    |java.lang.ClassCastException: dotty.tools.dotc.ast.Trees$Literal cannot be cast to scala.quoted.Expr
[error]    |	at sql2json.cat.Show$.$anonfun$1(Show.scala:32)
[error]    |	at scala.collection.immutable.List.foreach(List.scala:305)
[error]    |	at sql2json.cat.Show$.showInterpolatorImpl(Show.scala:38)
[error]    |
[error]    | This location is in code that was inlined at ShowInstancesTest.scala:10

For posterity, this is my non-working version:

  def showInterpolatorImpl(sc: Expr[StringContext], argsExpr: Expr[Seq[Any]])(given qctx: QuoteContext): Expr[String] = 
    import qctx.tasty.{_, given}
    val shownArgs: List[Expr[String]] = argsExpr.unseal.underlyingArgument match 
      case Typed(Repeated(args, _), _) =>
        args.foreach { 
          case '{ $arg: $t } => 
            searchImplicitExpr[Show[$t]] match
              case None =>
                qctx.error(s"No Show instance found for $arg", argsExpr)
                '{""}
              case Some(showImpl) => 
                '{$showImpl.show($arg)}
        }
        Nil
      case _ =>
        qctx.error("Expected statically known argument list", argsExpr)
        Nil

    '{ $sc.s(${Expr.ofList(shownArgs)}) }

  inline def (sc: => StringContext) show (args: Any*): String = ${ showInterpolatorImpl('sc, 'args) }

Or here is an alternative without macros. That pushes the implicit search into an implicit conversion.


object Lib {
  def (sc: StringContext) showMe(args: Showed*): String = sc.s(args: _*)

  opaque type Showed = String

  given [T](given show: Show[T]): Conversion[T, Showed] = show(_)

  trait Show[T] {
    def apply(x: T): String
  }

  given Show[Int] = x => s"Int($x)"
  given Show[String] = x => s"Str($x)"
}
object Test {
  import Lib._
  def main(args: Array[String]): Unit = {
    println(showMe"${1: Int} ${"abc": String}")
    println(showMe"${1} ${"abc"}")
    println(showMe"${1} ${println("xyz"); "xyz"}")
  }
}

Thatā€™s basically the Scala 2 implementation, unfortunately (or fortunately, depending on your view of implicit conversions), unless my build was just doing something weird, you need to either import the feature flag in both the definition scope and callsite scope, or you need to enable the compiler flag for implicit conversions.

As I mentioned above, this makes no sense as an opt-in path for something that exists to make code easier to reason about.

I wonder: should newer, Conversion-based implicit conversions still require the language import?

I donā€™t think they need it. But I had to make it over an opaque type and within the same module to allow it to be visible without other imports.

Current master seems toā€¦

12:57:14 $ bin/dotr
Starting dotty REPL...
scala> class Token(str: String)
// defined class Token

scala> given Conversion[String, Token] = new Token(_)
def given_Conversion_String_Token: Conversion[String, Token]

scala> def x: Token = "foo"
1 |def x: Token = "foo"
  |               ^^^^^
  |Use of implicit conversion trait Function1 in package scala should be enabled
  |by adding the import clause 'import scala.language.implicitConversions'
  |or by setting the compiler option -language:implicitConversions.
  |See the Scala docs for value scala.language.implicitConversions for a discussion
  |why the feature should be explicitly enabled.
def x: Token
1 Like

Notably, this proposed feature has landed in C# 13. Not quite as specced here (they only transform the varargs param, and not the entire method call) but pretty close and served a similar purpose

3 Likes