The missing pieces of new-style implicit conversions

From the latest core team meeting notes regarding the future of implicit conversions:

There are two questions here, the timetable of introducing warnings and errors, and the longer-term status of implicit def itself. We may need a fresh round of community input in order to find out how many libraries are still dependent on implicit def (for real reasons, not just inertia). Seb has proposed considering keeping implicit def, if it has turned out that Conversion isn’t powerful enough. The warning for Conversion will most likely be merged into 3.10 becoming an error from 3.11.

TLDR; No, Conversion is not powerful enough.

Use-case Coverage

I think there is no disagreement in the community that the old-style implicit def can be too surprising and too powerful without good guardrails, especially the global implicit defs. However, the new Conversion mechanism along with the into modifier currently fails to cover all reasonably safe uses.

Missing new-style inline conversions

Both inline implicit def and transparent inline implicit def have use cases. This where the type information given to Conversion is not enough and we need the actual tree. This mechanism is used in libraries like iron and refined to check the precise source type and fail with a relevant message.

Missing new-style from modifier

into helps only where we own the target type or the definition site. But there are cases where we own the source type and want to be able to implicitly convert to the target type. It is common to have Wrapper[T](t: T) with a conversion Wrapper[T] -> T. I want a from modifier to place on Wrapper so this conversion can be applied everywhere without any redundant flags.

Conversion vs. Extension

It’s important to distinguish between use sites where implicit conversions are meant for converting x: X -> conv(x: X): Y and where they are meant for old-style extensions (x: X).a -> (conv(x: X): Y).a.

The current plan to treat both the same when it comes to deprecation is wrong, IMO. I think we need a separate warning/error deprecation mechanism where old-style extension and old-style conversion is used

Old-style extension deprecation

We need to differentiate between implicit class and implicit def. One of the most surprising behavior of old-style implicits is the extension mechanism manifesting from implicit def. I think this pattern is the first one we should deprecate, separately from implicit class where that intent is clear.

As for deprecating implicit class, I would first want to see if we can still mitigate this issue Relaxed extension methods (SIP 54) are not relaxed enough

Old-style conversion deprecation

Here I think we need to separate the deprecation between places where we own either the target or the source type and between the horrible global implicit conversions and between inline and non-inline conversions. Truly global non-inline implicit conversions - deprecate now. The rest require mitigation with relevant language features like from and inline Conversion (or no deprecation at all if there is no fallback).

7 Likes

Is there any hope that from modifier will be implemented?

OK, I’m going to ask: given that old-style implicit conversions do a bunch of things that Conversion can’t do, like dependent types, inline and whatnot, what is the point of even having Conversion, except as a compatibility thing for those people who have already migrated?

The main argument against the implicit keyword is that it was ambiguous because it was used for many different purposes:

  1. implicit conversions (when passing parameters to a method)
  2. extension methods (when attempting to call a method that doesn’t exist on an object)
  3. implicit parameters
  4. definitions that make implicit parameters available

Nowadays, 2-4 are covered by extension, using and given, respectively, meaning that implicit is actually no longer ambiguous. So can’t we just have that keyword mean “implicit conversion” and be done with it?
Of course this raises questions on how implicit def and implicit class should behave in the future.
Here are some ideas we might consider:

  1. subject implicit def to the same restrictions as Conversion, i. e. only apply it for into parameters
  2. don’t apply implicit def conversions when calling an unknown method on an object - that’s what extension methods are for
  3. don’t apply implicit class conversions when passing a parameter - implicit class is really only meant for extension methods, so we can restrict it to that
  4. allow an implicit def foo(a: A): B to spawn a given Conversion[A, B] where that’s possible. It currently only spawns a given A => B

It was suggested that we might keep old-style implicit conversions around, but with a new syntax, and Martin appears to be open to that suggestion:

But I wonder: why do we even need a new syntax? Why not just identify and deprecate and eventually remove the subset of behaviours that we don’t like about it? We should avoid pointless code churn where we can.

3 Likes

I agree with @mberndt here (a new type is not the right solution unless it can do everything methods can do), except I feel much more strongly with respect to the discussion on “new syntax”. The effects of the 3.6 given syntax mainly give me the impression that people don’t like it and/or don’t understand it:

  1. Most of the hits for searching given syntax in the Scala Discord boil down to “this is confusing” / “the docs say X but only Y works” / “I’m shocked that’s even valid syntax”
  2. If you look at scalaVersion := "3.3.8" vs scalaVersion := "3.8.4" on GitHub, the numbers are about the same, but given [A]:/given[A]( has way more hits than given [A] =>
  3. And the hits for the new given syntax are cut in half if you exclude projects about the Scala language itself (i.e. excluding syntax examples used in books, the Scala documentation repository, Scalameta’s parsing tests, intellij-scala’s parsing tests)

My belief: things that quack like methods should also look like methods, and in that way the 3.6 given syntax is a pretty big blunder from my perspective. I don’t have any visions of a structured proposal, but I wanted to say something before I see the same things happen for conversions and end up too late to the conversation like I did for 3.6 given.

With the above, my ideal solution probably just looks like only keeping implicit def name[ZeroOrMoreTparams, ...](singleParameter: T)(using zeroOrMoreUsings: U1, ...): R = ... and proceeding with the deprecation of the other cases of implicit.

3 Likes

I suppose we could change one thing about it: make the name bit optional, like it is for given definitions.

I think the standard argument for new syntax in these cases is to prevent churn, by having existing code work exactly as before, while allowing people to migrate at whatever pace (never?).

There’s a tradeoff here.
Adding a new syntax for implicit conversion definitions:

  • existing code keeps working without changes
  • when you want to migrate to the new style, you have to change both the implicit conversion definitions as well as add into where you want them to be applied

Leaving the syntax for implicit conversion definitions the same, but applying the into restriction:

  • need to add into to existing code in order to keep it working
  • …but doesn’t require changes beyond that, the implicit conversion definitions can stay the same

So the question is, which one is more important to us: keeping old code working without changes, or making it easier for people to migrate to the new way of doing things?

I personally feel that, assuming a reasonable deprecation cycle, the second one is the better option. You don’t really get much benefit out of restrictions like “implicit conversions require into” if you don’t actually apply them consistently. Martin has also been pretty unambiguous about unrestricted implicit conversions going away.

Hmm. I think we may just be not on the same page what “new syntax” entails. I think we agree that keeping implicit def/class/object … seems to be more reasonable than coming up with something like an extended Conversion as the latter seems to be kinda not that great already (its not terrible, but too limited).

But I think a lot of work – that is not just a search&replace – will likely be due to having to work around the restrictions (at least for my remaining uses it would need a lot of into ) and why bother now, no one would gain anything.

If you consider it a given (hah) that Martin will make good on his 6+ year old prophecy that implicit def will stop working soon™, then yes, just reusing it seems like the best option.

Honestly I think it’s not a huge deal. This should be pretty easy to automate, either with -rewrite or by having an LLM do it.

Actually I see it a bit differently. I agree that implicit def should stay since Conversion isn’t able to do its job, as @soronpo has laid out. implicit class on the other hand is meant to be used for extension methods, and since we have a separate construct for that nowadays, I see little value in keeping it around. And implicit object is a separate issue because it doesn’t define an implicit conversion, which is what this thread is about.

2 Likes