The object in itself shouldn’t be inlined. This should work as instance inline methods already do.
inline val andConstraint: AndConstraint = AndConstraint(Positive, Lesser[3])
andConstraint.assert(8) //Inlined to `false`
andConstraint.left //Inlined to the Constraint instance
Inline variables/methods are already possibles. The actual proposal is to add the possibility to pass inline values to class/trait trough its constructor.
A use case could be the generation of some sort of inline functional interface (like the above Constraint) because Dotty doesn’t allow anonymous inline instance directly due to nested inline methods:
inline val LesserConstraint[V <: Int] = new Constraint {
override inline def assert(value: Int): Boolean = value < constValue[V]
}
//But this actually works
class Lesser[V <: Int] extends Constraint {
override inline def assert(value: Int): Boolean = value < constValue[V]
}
inline val LesserConstraint[V <: Int] = new Lesser
For this specific use case, a solution could be the authorisation to do the inline override directly in an anonymous class, even when the class is directly declared inside an inline method. We can already work around this so the compiler should be able to do that (see the above example).
This sounds rather interesting. Could it also prevent creation of unnecessary objects in code such as 1.to(100).map(...) (would the range not be created each time?)?
I see your point but it actually can’t do that by itself because it requires compile-time execution (like in a macro) but it can simplify the process~ forgot to remove this part when writing my post. Sure it does ! (see below)
Default FromExprs can retrieve the value of an inline variable or a chain of inline. Adding the inline parameter concept allow us to retrieve the value at compile-time, even trough classes. Then, we can compute it.
In your example, you can reproduce that with inline constructor parameters:
import scala.quoted.*
class Range(inline from: Int, inline to: Int) {
inline def inlinedMap[T](function: InlinedFunction[Int, T]): Seq[T] = {
var i = from
val buffer: mutable.Buffer[T] = mutable.Buffer()
while(i < to) {
buffer += function(i)
i += 1
}
buffer
}
}
This code:
val seq = Range(1, 100).inlinedMap(_ * 2)
Will desugar into
val seq = {
var i = 0
val buffer: mutable.Buffer[T] = mutable.Buffer()
while(i < 100) {
buffer += i*2
i += 1
}
buffer
}
I have to admin I didn’t work that much with monads. It looks interesting. I will dig into this but shouldn’t this simply work the same way as the inlinedMap I sent above ?
EDIT: Do you mean avoid the re-composition of the final function when chaining ?
I didn’t receive any reply so I will assume I’m right and you asked about function re-composition.
Firslty, let’s make a summary.
Introduction
The new inline keyword actually offers many possibilities:
Compile-time optimizations
Pre-computed return types (transparent inline)
Better way to retrieve value at compile-time using a macro (FromExpr)
Actually this only applies on methods and vals. Here I propose to add the possibility to inline constructor parameters.
Motivation
We can’t actually fully inline values passed through a class. While it’s generally not necessary to fully inline a value through a class, it is required in some cases.
With this implementation, a new Monad will be created at each call sequentially this can lead to performances issues, for example if we use monadic collections. With inline parameters, we’re able to precompute the entire chain into one, composed, function.
The method is declared as returning Dummy, so the behavior that it actually does so is entirely unsurprising. Yeah, you don’t get the benefit of completely removing the Dummy instantiation, but as that’s exactly what the code says it does, that might not be a problem.
To clarify, I’m not sure it should be desugared in all cases.
Another way of stating this would be that I don’t think it would be inconsistent to give callers the type they ask for, and that these existing together are both internally internally consistent and predictable:
Original
case class Dummy(inline value: String)
def materialized(v: String): Dummy = Dummy(v)
def partiallyInline1(v: String): String = Dummy(v).value
inline def partiallyInline2(v: String): Dummy = Dummy(v)
inline def completelyInline(v: String): String = Dummy(v).value
val result1 = materialized("Hello World").value
val result2 = partiallyInline1("Hello World")
val result3 = partiallyInline2("Hello World")
val result4 = completelyInline("Hello World")
Desugared
case class Dummy(inline value: String)
def materialized(v: String): Dummy = Dummy(v)
def partiallyInline1(v: String): String = v
inline def partiallyInline2(v: String): Dummy = Dummy(v)
inline def completelyInline(v: String): String = v
val result1: String = materialized("Hello World").value
val result2: String = partiallyInline1("Hello World")
val result3: Dummy = Dummy("Hello World")
val result4: String = "Hello World"
Edit: tried to get them side-by-side for better readability, but it wouldn’t scroll horizontally
Maybe? The compiler should flag it if they aren’t expecting a Dummy, so maybe only warn if the type wasn’t explicitly annotated (no idea if that’s possible)?
Basically, if a method isn’t declared inline, users should reasonably be expected to understand that the method isn’t going to be inlined and it’s going to behave like a standard method. I’m not sure that warning it won’t do that is all that helpful.
On the other hand, if the inlining is part of the implementation, and the type isn’t explicitly annotated, then I could see a bug like this getting written, and the compiler complaining:
I’m not sure the compiler can be reasonably expected to keep track of situations like this, and somehow inline everything at result:
// Dummy.scala
case class Dummy(inline value: String)
object Dummy {
def method(v: String): Dummy = Dummy(v)
}
// Client.scala
val result = Dummy.method("Hello World").value
On the other hand, if you’re building a DSL that leverages this to inline things in all sorts of wonderful ways, and someone wants to do something to extend that DSL in a way that requires passing a Dummy around a bit, silencing that warning every time you call Dummy.value is going to get pretty annoying.
I’m not sure the compiler can be reasonably expected to keep track of situations like this, and somehow inline everything at result
I’m not saying the compiler should inline Dummy.method("Hello World").value. Due to its inline nature, users can think that the above code will be inlined into "Hello World" but here, that’s not the case because method isn’t inline.
I (currently) don’t see a use case for:
// Dummy.scala
case class Dummy(inline value: String)
object Dummy {
def method(v: String): Dummy = Dummy(v)
}
// Client.scala
val result = Dummy.method("Hello World").value
and that’s why I think the compiler should warn the user about a possibly unexpected behaviour (here, Dummy.method("Hello World").value not being inlined to "Hello World")
It would make sense that this should work, particularly if you want to provide some extra assurance that pure has “exactly once” semantics (and the cost of an allocation may well be worth it to gain that guarantee).
Monad
.pure(5)
.map(_ * 2)
.map(_ / 10)
.value
Because Monad.pure isn’t inline, this could be reasonably be desugared like this:
I’m not really sure if extensions methods can be defined inline, but even if they can, it might not be worth bothering with the extra complexity if you’re just writing a quick helper for a test class.
It would make sense that this should work, particularly if you want to provide some extra assurance that pure has “exactly once” semantics (and the cost of an allocation may well be worth it to gain that guarantee).
Ok so there is currently no issue with the above summary ? Great
I grabbed it because it actually exists in Cats, and it came to mind because the example was a Monad, and it was a method I knew there was a pre-existing usecase for.
Yep, at least as far as I can tell, it’s currently internally consistent and unsurprising as-is.
As an understanding check, is this how you understand the stepwise inlining would go for the Range example?
Class definition
This is identical to the summary above, I’m copying it here for reference
class Range(inline from: Int, inline to: Int) {
inline def map[T](function: InlinedFunction[Int, T]): Seq[T] = {
var i = from
val buffer: mutable.Buffer[T] = mutable.Buffer()
while(i < to) {
buffer += function(i)
i += 1
}
buffer
}
}
Base
val seq = Range(1, 100).inlinedMap(_ * 2)
Stage 1
I’m going to handwave the compiler generating a unique identifier for i, to, from, etc
val seq = {
val r = Range(1, 100)
r.inlinedMap(i => i * 2)
}
Stage 2
val seq = {
val r = Range(1, 100)
var i = r.from
val buffer: mutable.Buffer[T] = mutable.Buffer()
while(i < r.to) {
buffer += i * 2
i += 1
}
buffer
}
Stage 3
This caches the inline parameters, to avoid multiple execution, which I think is consistent with the inline constructor parameters being by-value vals, rather than being by-name.
The implications of this is that, if we want to be able to have by-name semantics, the user would have to supply a wrapper and have the actual inline parameter be a function value.
val seq = {
var i = 1
val buffer: mutable.Buffer[T] = mutable.Buffer()
while(i < 100) {
buffer += i * 2
i += 1
}
buffer
}