Proposal: Add `transparent inline` parameters

I would like to propose the addition of transparent inline parameters to the language.

They can only be used with inline methods and they would be similar to normal inline parameters, with the difference that their usage won’t be type-checked at the definition site, but at the usage site.
Thus, making them very similar to C #define functions, allowing users to generate simple code blocks without needing to resort to more advanced tools like macros.

A motivating example of when they would be useful is something like this:

// Imagine you have the following repetitive code.

val data = List(
  someRepetitiveMethodCall(foo).anotherRepetitiveMethodCall,
  someRepetitiveMethodCall(bar).anotherRepetitiveMethodCall
  // ...
)

One would be tempted to simplify that as:

val data = List(foo, bar, ...).map { x =>
  someRepetitiveMethodCall(x).anotherRepetitiveMethodCall
)

However, in this case, foo, bar, etc don’t share any common supper type; thus x becomes Any. And, someRepetitiveMethodCall actually requires some implicit evidence; thus needing the precise type of each element.
One may create a small helper to reduce the number of characters, but that may require importing a bunch of things, adding a lot of type parameters, repeating the implicit parameters, etc; which could lead to little code / noise reduction

Note that, the current inline support is not enough to define such a helper, since the following would not compile; because of a type mismatch error.

transparent inline def f(inline x: Any) =
  someRepetitiveMethodCall(x).anotherRepetitiveMethodCall

Thus, I am proposing the possibility to tag x as transparent inline and thus allowing us to write the previous function, which then would be used like this:

val data = List(
  f(foo),
  f(bar),
  // ...
)

The code would be expanded as the original one.


On a complementary note, I also thought of having an inline for (or something similar).
Which would have two variants: A foreach one, which would just generate independent lines of code. And a yield one, which would collect all emitted elements on a List.

I imagine something like this:

foreach

inline for(foo, bar, baz) { x =>
  doSomethingWith(x)
}

yield

val data = inline for(foo, bar, baz) yield { x =>
  doSomethingWith(x)
}

Which would be expanded as (respectively):

foreach

doSomethingWith(foo)
doSomethingWith(bar)
doSomethingWith(baz)

yield

val data =
  doSomethingWith(foo) ::
  doSomethingWith(bar) ::
  doSomethingWith(baz) ::
  Nil

The block would follow the normal inline rules, reducing the expression all it can.

I am not particularly fond of the syntax, just the idea.
But also, I understand that this second proposal is more controversial than the first one, which is why I did s soft split; if a moderator thinks is better to split it into a different post either let me know or feel free to do it :slight_smile:


Looking forward to reading your feedback.

Why not use a type-parametrized f:

def f[T : Something](x: T) =
  someRepetitiveMethodCall(x).anotherRepetitiveMethodCall
transparent inline def f(inline x: Any) = ???

such f will be impossible to refactor or reason about.

Again, in the specific example when I needed that, the resulting function was something like:

def f[Alg[_, _, _, _, _], F[_]](x: Foo[Alg, F])(using ev1: Bar[Alg], ev2: Baz[F], ev3: Quax[...]): Faux[F, XYZ[F]] =
  someRepetitiveMethodCall(x).anotherRepetitiveMethodCall

And none of the types were imported in the current scope.
Thus, adding the helper didn’t actually reduce the amount of code, and it was just more noise than copying and pasting a single line.

And that is when I got the idea, I didn’t want to write a good reusable function; since those already existed (someRepetitiveMethodCall & anotherRepetitiveMethodCall). All I wanted was a simple way to generate code.


I don’t disagree with that actually, but I don’t think is much worse than normal transparent inline.
Sure, a transparent inline parameter is probably something you don’t want on a public method of a library, but is not a big deal if it is used on a closed scope.

Of course, I would never encourage anyone to make them their primary tool of abstraction.
If anything, if you please, a counter-proposal to this one that would solve my same meta-problem would be to allow a method to infer the type of their input parameters; like x: auto.
Again, nothing you would want on a public API, but very handy for closed scopes.

I’m not sure if that would work, but have you tried x: ?

(? is the wildcard type in Scala 3)

I’m afraid this proposal goes contrary to an essential design goal of inline methods in Scala: that their body can be typechecked and elaborated once and for all, with a guarantee that the same elaboration is used at every call site.

What you’re asking for is syntactic macros with unpredictable elaboration. When designing Scala 3 macros we made a conscious decision not to support anything like that, and I don’t see that main design decision changed anytime soon (or ever).

4 Likes

I suspect this is actually a case for summonInline.

transparent inline def f[A](inline x: A) =
  someRepetitiveMethodCall(x)(using summonInline[TypeClass[A]]).anotherRepetitiveMethodCall

For your real use case perhaps that will still require quite complex code. Which brings us back to this old suggestion of mine:

1 Like

I would love to have something like this for some usecases in perspective. For me I would love to have them both as parameters and also working with lambdas. However, I have long suspected what @sjrd mentioned, and do not have much hope for a feature like this. What I have done for one case where it was the most important was to just use macros instead. An example of such a usecase is below.

val list = gen.tabulateFoldLeft(Nil: List[(String, Json)], unrolling = true)((acc, idx) =>
  val js = gen.lateInlineMatch {
    a.productElementIdExact(idx) match {
      case p: Byte    => Json.fromInt(p)
      case p: Char    => Json.fromString(p.toString)
      case p: Short   => Json.fromInt(p)
      case p: Int     => Json.fromInt(p)
      case p: Long    => Json.fromLong(p)
      case p: Float   => Json.fromFloatOrString(p)
      case p: Double  => Json.fromDoubleOrString(p)
      case p: Boolean => Json.fromBoolean(p)
      case p: String  => Json.fromString(p)
      case other      => encoders.indexK(idx)(other)
    }
  }

  (names.indexK(idx), js) :: acc
)

lateInlineMatch works line an inline match here, but is expanded later than an inline match would be.