Skip to content

SIP-71: Allow fully implicit conversions in Scala 3 with into #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Apr 23, 2025

No description provided.

@soronpo
Copy link
Contributor

soronpo commented Apr 23, 2025

Needed section: Scala 2 interoperability, considering into won't be added there.

Copy link

@vascorsd vascorsd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, when I initially heard about this feature I had a gut feeling I didn't like it, but after reading it carefully I think I changed my mind. It's surprisingly interesting.

I feel the feature and the easiness of the syntax will be abused heavily by some users and probably new users since they will get into contact with it pretty early in their lives in scala since eventually this will all end up in signatures of the standard library almost everywhere and users tend to copy the patterns they see in their language std libs since that's what they believe are good patterns for writing code in the language.

It's gonna for sure generate a lot of contention to explain to users why they should or should not use this instead of typeclasses and why should they even think about them if they have all of this mechanism. Would be interesting to see a deeper exploration on that, the limits and when to choose this encoding and how to guide users to good choices and specially to avoid using into all over the place.

Guess we'll have to wait and see how it all pans out.

content/into.md Outdated
Comment on lines 209 to 218
- a type of the form `into[T]`,
- a reference `p.C` to a class or trait `C` that is declared with an `into` modifier,
which can also be followed by type arguments,
- a type alias of a valid conversion target type,
- a match type that reduces to a valid conversion target type,
- an annotated type `T @ann` where `T` is a valid conversion target type,
- a refined type `T {...}` where `T` is a valid conversion target type,
- a union `T | U` of two valid conversion target types `T` and `U`,
- an intersection `T & U` of two valid conversion target types `T` and `U`,
- an instance of a type parameter that is explicitly instantiated to a valid conversion target type.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to seem some more examples listed with all these combinations, specially union and intersection types, etc. For exhaustiveness and to get a feeling when combined in these situations.


`into` is a soft modifier. It is only allowed on traits and classes.

## Compatibility

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, in library code that I publish if I change a parameter or a class to now have into, from a user of that library it will not break and if I do the opposite too, for example if I change my mind later and change it back to not have the into?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing back or forth it is binary and Tasty compatible. But dropping it will mean that user code that relies on implicit conversions into the trait will get a warning.

```
Here, the type variable of `List.apply` is not explicitly instantiated
when we check the `List(...)` arguments (it is just upper-bounded by the target type `into[Keyword]`). This is not enough to allow
implicit conversions on the second and third arguments.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps explain here in simpler terms why it is "not enough"? And perhaps give an example that show how to make it type check? Also you sometimes use "expected type" and sometimes "target type" - is there a reason for this? I think "expected type" is easier to understand without specific type inference knowledge, while "target typing" is a more advanced concept...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use "target" in "conversion target types" since its shorter than "expected types of conversions" and lends itself better to a formal definition.

content/into.md Outdated

```scala
def concatAll(xss: into[IterableOnce[Char]]*): List[Char] =
xss.foldLeft(List[Char]())(_ ++ _)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a quadratic cost but a not uncommon kind of mistake. Can we use something as an example that isn't an anti pattern? For instance prepending with a foldRight for instance, or xss.flatten.to(List)

Also:

 - add section on Scala 2 compatibility
 - improve concatAll example
```
Inside the `++` method, the `elems` parameter is of type `IterableOnce[A]`, not `into[IterableOne[A]]`. Hence, we can simply write `elems.iterator` to get at the `iterator` method of the `IterableOnce` class.

Specifically, we erase all `into` wrappers in the local types of parameter types that appear in covariant or invariant position. Contravariant `into` wrappers are kept since these typically are on the parameters of function arguments.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the fact that the type of into is meaningful from the outside (because of the lower-bound) but needs to be erased inside (because the lower-bound doesn't let you call any method on it) is more confusing than helpful. Either into behaves like a normal citizen of the type system and I can understand if something typechecks from looking at the definition of into (e.g. if it's type into[T] = T), or it's outside the type system (and then it seems like it really is more like a keyword like =>).

This would mean we have to add `into` to the whole `Expr` enum. Adding it to just `Const` is not enough, since `Add` and `Neg` take `Expr` arguments, not `Const` arguments.

But we might not always have permission to change the `Expr` enum. For instance, `Expr` could be defined in a lower level library without implicit conversions, but later we want to make `Expr` construction convenient by eliding `Const` wrappers in some higher-level library or application. With `into` constructors, this is easy: Define the implicit conversion and facade methods that construct `Expr` trees while taking `into[Expr]` parameters.
With `into` modifiers there is no way to achieve the same.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be solved by allowing into as a modifier on type aliases. Then, the higher-level library is free to define:

into type intoExpr = Expr

@kyouko-taiga kyouko-taiga changed the title New SIP: Allow fully implicit conversions in Scala 3 with into SIP-71: Allow fully implicit conversions in Scala 3 with into Apr 25, 2025
@sjrd sjrd self-assigned this Apr 25, 2025
Comment on lines +178 to +179
To facilitate migration, we also introduce an alternative way to specify target types of implicit conversions. We allow `into` as a soft modifier on
classes, traits, and opaque type aliases. If a type definition is declared with `into`, then implicit conversions into that type don't need a language import.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the into modifier be inferred if the companion object contains implicit conversions? This would make migration unnecessary in a lot of cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular point has been discussed during the last the SIP committee. The general observation is that information going into a public API should not be inferred; they should be conscious decisions. Hence, the into modifier should not be inferred.

@SimY4
Copy link

SimY4 commented Apr 26, 2025

Just curious, why opaque alias is preferred over annotation @into? This seems as a good fit for altering compiler behaviour via additional context passed as an annotation on a type.

I can foresee into type appearing in compiler errors (since it's opaque) complicating the understanding the code.

@odersky
Copy link
Contributor Author

odersky commented Apr 26, 2025

Just curious, why opaque alias is preferred over annotation @into? This seems as a good fit for altering compiler behaviour via additional context passed as an annotation on a type.

No it's the opposite. Annotations should not affect typing. They can affect host interop and code generation though. That's for instance why infix and inline are modifiers, not annotations. Also, annotations do not get propagated reliably through type inference. For instance they can be dropped by meets and joins.

@sjrd
Copy link
Member

sjrd commented May 23, 2025

FTR, I recommend to accept this proposal.

The motivation is well laid out. The proposed solution addresses all the concerns I've had over the years about deprecating implicit conversions. Also, I don't think we can make the proposed solution any simpler without weakening it. In other words, IMO this is an excellent proposal.

I would still like to see somewhere written the reason why into[T] >: T, instead of the more intuitive into[T] <: T. @odersky's explanation offline convinced me at the time, but it should be recorded for posterity.


`into` is defined as follows in the companion object of the `scala.Conversion` class:
```scala
opaque type into[T] >: T = T
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commenting here to keep track of the loose idea of defining this as

opaque into type into[T] >: T = T

(not sure of the recommended order of opaque and into modifiers though).
Would that make any sense? Could that simplify the SIP by making the type into less special?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.