typelevel / literally

Compile time validation of literal values built from strings
Apache License 2.0
106 stars 6 forks source link

RFC: embedded string interpolations #44

Open danicheg opened 2 years ago

danicheg commented 2 years ago

This issue is a follow-up from https://github.com/http4s/http4s/issues/3433. I've struggled with this a bit and think it's quite problematic to embed string interpolations to uri"" and path"" of http4s (actually, it's so for every interpolated macros based on literally):

  1. At the moment, compilation fails if invalid strings have been passed (underlying validate is calling at compile time).
  2. Say
    val foo = "/foo/bar"
    val someuri = uri"https://example.com$foo"

    means that we should check the value of foo at the runtime (generally, foo could be some computation, not a constant value). So bringing embedded string interpolations means we should proceed with runtime values at compile time (because of 1). I'm not that much a Scala macros astronaut, but am feeling it's probably impossible.

UPD: speaking more accurately, compile-time checking is only possible for the literal part of the passed string.

If anyone has insights about that, please share them here.

mpilquist commented 2 years ago

Yes, that's correct. It's possible to support interpolation of constant values at compile time, but not possible to use non-constant values. Doing so would require changing the macro to return an Either[String, Uri] when there are one or more non-constant parts and Uri otherwise. This type switching would require a white box macro in Scala 2 and transparent inline in Scala 3.

To support interpolation of constant values, we could do something like this:


trait Literally[A]:
  type Quotes = scala.quoted.Quotes
  type Expr[A] = scala.quoted.Expr[A]
  val Expr = scala.quoted.Expr

  def validate(s: String)(using Quotes): Either[String, Expr[A]]

  def apply(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[String]])(using Quotes): Expr[A] =
    apply(strCtxExpr.valueOrAbort.parts, argsExpr.valueOrAbort)

  private def apply(parts: Seq[String], args: Seq[String])(using Quotes): Expr[A] =
    val str = parts.zipAll(args, "", "").map(t => t(0) + t(1)).mkString("", "", "")
    validate(str) match
      case Left(err) =>
        quotes.reflect.report.error(err)
        ???
      case Right(a) =>
        a

Usage:

scala> inline val x = "4"

scala> val y = port"1${x}2"
val y: org.typelevel.literally.examples.Port = Port(142)

This is too limited though as it requires the args list to be strings. It would be nicer to support arbitrary types there. Overall, I don't think the complexity is worth it.