improvement-proposals
improvement-proposals copied to clipboard
SIP-66 - Implicit macro conversions
This should be sip 66
This is also required for Mill, which uses a lot of macro implicit conversions for its Task DSL
Yes. That's why I included it in the Motivation section :+1:
@lihaoyi mentioned it first in this thread
Hello @smarter, do you have any news on it ? 🙏
This proposal was discussed in today's SIP committee meeting. There was general agreement that inline/macro implicit conversions do have important use cases and will need to be supported going forwards.
However the conclusion was that we can't make a decision about this proposal without first deciding on the future syntax and restrictions for regular implicit conversions. That may take some time - I don't think there's even a SIP for it yet, although there have been various discussions on the contributors forum.
I believe the intention is to retain support for the current implicit inline def syntax until we have a suitable replacement (whether from SIP-66 or some future proposal). For now we intend to leave this SIP open in review state.
One problematic aspect in the proposal is to have two type classes for Conversion and InlineConversion side by side. This means we now need two implicit searches instead of one each time we search for a conversion. That looks like a steep price to pay.
But there is an alternative, which I discovered when playing with another potential SIP. We can make InlineConversion an inline type class which is a superclass of Conversion, like this:
abstract class InlineConversion[-From, +To]:
inline def applyInline(inline x: From): To
extension (x: From) inline def convertInline: To = applyInline(x)
abstract class Conversion[-From, +To] extends InlineConversion[From, To]:
def apply(x: From): To
inline def applyInline(inline x: From): To = apply(x)
extension (x: From) def convert: To = apply(x)
When searching for an implicit conversion from A to B the compiler will search for an InlineConversion[A, B]. If the actual found instance is an InlineConversion, it will use its applyInline method to convert. If the actual found instance is a regular Conversion it can just call the apply method instead. It does not really matter since applyInline anyway forwards to apply in this case.
Another advantage of this scheme is that any questions how to disambiguate between inline and regular conversions are already answered by the standard given priority rules. That is, from 3.7 on inline conversion would win over conversion since it is more general.
I think that InlineConversion as a super-class could work yes.
Playing a bit with the example in the SIP I think there's another missing language feature needed to make it work though, the example given is:
implicit inline def autoRefine[A, C](inline value: A)(using inline constraint: Constraint[A, C]): A :| C =
macros.assertCondition(value, constraint.test(value), constraint.message)
IronType(value)
which the SIP suggets should become something like:
inline given autoRefine[A, C](using inline constraint: Constraint[A, C]): Conversion[A, A :| C] with
override inline def apply(inline value: A): A :| C =
macros.assertCondition(value, constraint.test(value), constraint.message)
IronType(value)
But if I try to actually implement this (switching to the InlineConversion proposal from @odersky's comment), I get an error:
//> using dep "io.github.iltotore::iron:2.6.0"
import io.github.iltotore.iron.*
abstract class InlineConversion[-From, +To]:
inline def applyInline(inline x: From): To
extension (x: From) inline def convertInline: To = applyInline(x)
inline given foo[A, C](using inline constraint: Constraint[A, C]): InlineConversion[A, A :| C] with
inline def applyInline(inline x: A): A :| C =
macros.assertCondition(x, constraint.test(x), constraint.message)
IronType(x)
[error] ./try/sip86.scala:9:37
[error] inline modifier can only be used for parameters of inline methods
[error] inline given foo[A, C](using inline constraint: Constraint[A, C]): InlineConversion[A, A :| C] with
[error] ^
Using a given alias doesn't work either:
//> using dep "io.github.iltotore::iron:2.6.0"
import io.github.iltotore.iron.*
abstract class InlineConversion[-From, +To]:
inline def applyInline(inline x: From): To
extension (x: From) inline def convertInline: To = applyInline(x)
inline given foo[A, C](using inline constraint: Constraint[A, C]): InlineConversion[A, A :| C] = new:
inline def applyInline(inline x: A): A :| C =
macros.assertCondition(x, constraint.test(x), constraint.message)
IronType(x)
[error] ./try/sip86.scala:10:14
[error] Implementation restriction: nested inline methods are not supported
[error] inline def applyInline(inline x: A): A :| C =
[error] ^
This looks non-trivial to fix (especially the given ... with form that desugars into a class where there's no obvious way to give a meaning to an inline parameter, the given alias desugars into a def and the implementation restriction against nested inline methods might not require too many changes)
I think we can allow nested inline methods if there is an intervening class. Not sure it will help, though. I disabled the no-nested inlines check in this example:
abstract class InlineConversion[-From, +To]:
inline def applyInline(inline x: From): To
extension (x: From) inline def convertInline: To = applyInline(x)
class Constraint:
inline def map(inline x: String): Int = x.length
given Constraint()
class StrToInt(constraint: Constraint) extends InlineConversion[String, Int]:
inline def applyInline(inline x: String): Int =
constraint.map(x)
inline given foo(using inline constraint: Constraint): InlineConversion[String, Int] = new InlineConversion:
inline def applyInline(inline x: String): Int =
constraint.map(x)
object Test:
val x = foo.applyInline("123")
val y = foo(using Constraint()).applyInline("123")
I get:
~/workspace/dotty/tests/new> sc test.scala
-- Error: test.scala:19:25 -----------------------------------------------------
19 | val x = foo.applyInline("123")
| ^^^^^^^^^^^^^^^^^^^^^^
|Deferred inline method applyInline in class InlineConversion cannot be invoked
-- Error: test.scala:20:45 -----------------------------------------------------
20 | val y = foo(using Constraint()).applyInline("123")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|Deferred inline method applyInline in class InlineConversion cannot be invoked
2 errors found
I am not sure to get the example. Would this mean that we'd need to explicitly use applyInline or convertInline?