-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Typeclass experiments refactored #20061
Typeclass experiments refactored #20061
Conversation
5a65e4b
to
f38d3c7
Compare
If we assume `tracked` for parameter `x` (which is implicitly a `val`), | ||
then `foo` would get inferred type `Foo { val x: 1 }`, so it could not | ||
be reassigned to a value of type `Foo { val x: 2 }` on the next line. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if the compiler detects all reassigments of this kind, then it could be tracked by default if no reassignments are detected? And if they are detected the compiler can explain in an error message why it is not tracked (and explain that other stuff like dependent typing inference will not work). With a tracked-by-default-if-no-dangers rule we will not force the coder to write tracked unnecessarily, as it is probably tracked
the coder wants if not doing reassignments, and it will be easier to work with dependent typing as you do not always need to bother with tracked. Would that work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is the ordering. At the time where we have to decide whether something needs to be tracked or not, we cannot see what the body of the class requires. Also, tracked
implies a parameter is turned into a val
, which means it can potentially clash with members defined in base classes. I believe this means we need to make it explicit. But we can certainly suggest to add tracked
in many error situations. Basically, whenever an error mentions a skolem like ?1.Out, it's likely a missing tracked and we can point out where to add one.
```scala | ||
class D extends C { type T = Int; def m(): 22 }: // error | ||
def next(): D | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps explain why this must be illegal?
**9.** The following change is currently enabled in `-source future`: | ||
|
||
**9.** Given disambiguation has changed. When comparing two givens that both match an expected type, we used to pick the most specific one, in alignment with | ||
overloading resolution. From Scala 3.5 on, we pick the most general one instead. Compiling with Scala 3.5-migration will print a warning in all cases where the preference has changed. Example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explanation of this would be good: Why is this change needed? (And what migration actions can be done in code that rely on the old behavior, so that it still works with the new behavior?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a separate PR: #19300
It is rolled into this PR because this PR contains examples where we have triangles such as in the parser combinators, where two type classes extend Combinator. The new rules are needed to resolve these better. And they seem not to break anything, empirically! #19300 contains a discussion in the comments.
|
||
|
||
## Auxiliary Type Alias `is` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I like is
better than forms
so good that you changed back! (I think forms
is esoteric lingo; I could also perhaps learn to like implements
but is
is shorter and does not stick in the eye as much, and is almost as silent as a colon but still carries reasonable semantics).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think is
indeed is less esoteric lingo. But it is semantically wrong how Scala is used today. If you look at upickle, play-json, circe, cats, zio-prelude, akka, etc. then you have type classes like Format
, Reader
, Writer
, Encoder
, Decoder
, Monoid
, Ordering
, Marhsaller
and similar. For all these things you would say has
and not is
.
If you want parties to migrate to Scala 3 you need to take into consideration what is in production today. And hypothetical type classes like Collection
and Readable
that are not used in real-world libraries (in Scala) should not take preference.
I know this is bike-shedding, but I don't think the reputation Scala currently has helps if these type of changes are made without broad consensus and support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So from here you have implicit val decodeFoo: Decoder[Thing] = new Decoder[Thing]{...}
which would be
given Thing is Decoder:
...
and you would like
given Thing has Decoder:
...
We could have both as it is just a Predef type alias.
But anyone could also if is
is too itchy:
type Decoded = Decoder
given Thing is Decoded:
...
and it reads a bit better.
Maybe it's because I'm a non-native English speaker that I'm more pragmatic about
given Thing is Decoder
I'm fine with it because it kind of is a Decoder in the sense that Thing now can do decoding, which is what the Decoder is doing for it behind the scenes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have both as it is just a Predef type alias.
Sure, but I do think has
is a much better default than is
if you look at popular type classes in Scala 2 today. Also it is IMO better to not give too much choice and stick with the most logical choice looking at how Scala is used.
given Thing is Decoder
IMO if you make Thing
concrete for actual types like Int
, String
and Person
the is
stands completely out of place. Now again, I know this is nitpicking. BUt I had the idea that there was much more consensus for has
the last time this discussion was held and it is ignored (again).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Aside: this thread comments a commit that is part of early experimentation even before a subsequent improvement proposal is submitted. No comments will be "ignored" as we will continue to discuss all trade-offs. All previous discussions is input to the SIP committee as we come to decision-making about this so I don't think it is fair to say "ignored (again)" here; this thread is open to the whole planet earth to read and comment and the SIP committee's intention is to be benevolent of all factual input and try to make good trade-offs based on what is included both in written proposals and in pointers to open discussions etc.)
Anyways, back to the factual matters: The proposal regards new type classes that use the new Self-type-member scheme; the old style wont go away as type parameters and given-using wont go away. So the naming of type-param-based type classes can stay the same. For new type-member-based type classes the naming can take the is
-type-alias into account. And the nice thing with is
being just a type alias in Predef is that anyone can define has
in a similar way locally.
I actually think this makes sense:
given Int is Decoder
if you think of it as the Int class is given the ability to be a Decoder of serialized integer values. You can read it as "given that Int is a Decoder then we can use Int to decode serialized Int values".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proposal regards new type classes that use the new Self-type-member scheme; the old style wont go away as type parameters and given-using wont go away.
That will mean that Scala will have 2 different ways of defining and using typeclasses. And in reality you can add implicit to that mix as well for as long as it is still being supported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that's a price to pay when embracing both innovation, stability and ease of migration. We should consider and balance trade-offs between stability and innovation in each case. And, after these experiments are turned into a proposal the SIP committee will take all input into account and vote to try to make the best decision. But this is still early days and I think it is good if these experiments that try to leverage the recent abilities of the type system can get out there in a reasonably stable "best-possible-for-now"-version for everyone to play with under a flag to give feedback and surface unanticipated problems etc.
|
||
**Proposal:** Introduce a new way to implement a given definition in a trait like this: | ||
```scala | ||
given T = deferred |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not call it abstract
instead of the perhaps more cryptic deferred
? After all it is called an "abstract given" in the text...
abstract given T
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Abstract givens are different. Here we have a concrete given from a users point of view. It's just that the implementation is spread out over subclasses. But you see that it is concrete because if you want to implement it yourself in a subclass you need an override
modifier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that to write an implementation with override
, one need to use the exact generated name, which might not be easy to guess (even though there are clear rules for these names).
trait Ord:
type Self
trait Sorted:
type Element: Ord
object Scoped:
given (Int is Ord)()
class SortedIntCorrect extends Sorted:
type Element = Int
class SortedIntCorrect2 extends Sorted:
type Element = Int
override given (Int is Ord)() as given_Ord_Element
class SortedIntWrong1 extends Sorted:
type Element = Int
override given (Element is Ord)() // error
class SortedIntWrong2 extends Sorted:
type Element = Int
override given (Int is Ord)() // error
so I would expect reasonable code to be like SortedIntCorrect
instead of SortedIntCorrect2
.
given String is Ord: | ||
def compare(x: String, y: String) = ... | ||
|
||
given [A : Ord] => List[A] is Ord: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like an explanation here of what the syntax with the rocket means. I could not find any example with rockets in the other existing doc pages on given instances etc. There are a lot of symbols here, colon and brackets and arrows and what not so an explanation will help the reader a lot (at least me :) ).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This currently looks very weird to me, as I am used to think about given
s as special val
s or def
s. (Anyway, probably not the place and time to discuss the syntax.)
starts migrating. | ||
|
||
Migration to the new syntax is straightforward, and can be supported by automatic rewrites. For a transition period we can support both the old and the new syntax. It would be a good idea to backport the new given syntax to the LTS version of Scala so that code written in this version can already use it. The current LTS would then support old and new-style givens indefinitely, whereas new Scala 3.x versions would phase out the old syntax over time. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this reasoning makes sense. In 10 years I think we will find that this was the right decision even if the change-it-again-after-recent-change-scenario is painful.
def pure[A](x: A): List[A] = | ||
List(x) | ||
|
||
type Reader[Ctx] = [X] =>> Ctx => X |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks hairy. Perhaps explain with a comment what it means, and refer to relevant doc pages.
About naming "is/Self" vs "forms/Something else": Note that the The reasoning for "is/Self" is as follows. With this PR, we use context bounds Now, if In light of the adjective analogy, maybe we we should tweak the names of our example type classes some more. Should be |
If you excuse a well meant comment from the peanut gallery 🙏 If we want to go ahead with using given Int is Ord: we should also use def minimum[T is Ord](xs: List[T]) = or given [T is Ord] => List[T] is Ord: I think retiring We can keep Note that my proposal doesn't really care whether it is (And we could even drop the given T is Ord => List[T] is Ord: |
I think this is focusing too much on small details -
But then you are using |
It's this vagueness which people will find confusing.
I am certain that is not how people will think of it. And not just the beginners, but ordinary Scala developers. They wouldn't think about the first par as using keywords and the other part as using infix types. For them, it would be the same thing. If anything, this shows that if we really want to improve the Type Class situation in Scala, it would be desirable in the part which is currently "infix-type at use-site (List[T] is Ord)" to turn the |
76d519c
to
fccba2b
Compare
One new thing over the previous PR is that we fix is the handling of trait Singleton extends Any:
type Self That's compatible with the previous subtyping constraints, but also allows the new context bound syntax. Furthermore, we treat |
4f1fc94
to
0be5237
Compare
We now also implement precise type inference using a |
f72b090
to
706bcd1
Compare
Regarding the Precise type class, does it also keep tuples precise? |
I believe the answer will be "it depends". A precise type variable gets instantiated without widening of singletons or unions at the top-level.vA variable is precise if it is declared with a Great idea to check behavior and expressiveness using the test cases of #15765. |
706bcd1
to
7887fe1
Compare
c6f7c74
to
cf0a97e
Compare
*/ | ||
@experimental | ||
infix type is[A <: AnyKind, B <: Any{type Self <: AnyKind}] = B { type Self = A } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it still appear experimental in TASTy? otherwise I guess we'd have to make this PR needs-minor-release
to avoid leaking to prior compilers in same minor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will hopefully get into 3.5
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will hopefully get into 3.5
"self" and "is" both will be added in scala 3.5?
We already reduce `R { type A = T } # A` to `T` in most situations when we create types. We now also reduce `R { val x: S } # x` to `S` if `S` is a singleton type. This will simplify types as we go to more term-dependent typing. As a concrete benefit, it will avoid several test-pickling failures due to pickling differences when using dependent types.
If a context bound type `T` for type parameter `A` does not have type parameters, demand evidence of type `T { type Self = A }` instead.
Allow to constrain type variables to be singletons by a context bound [X: Singleton] instead of an unsound supertype [X <: Singleton]. This fixes the soundness hole of singletons.
Now: 3.5: old scheme but warn if there are changes in the future 3.6-migration: new scheme, warn if prioritization has changed 3.6: new scheme, no warning
If they are illegally used as values, we need to return an error tree, not a tree with a symbol that can't be pickled.
Error number changed
d060838
to
e9bddd5
Compare
e9bddd5
to
3c78ada
Compare
A refactoring of the typeclass-experiments branch according to topics.
DISCLAIMER: This is the same as the previous typeclass-experiments Pre-pre SIP. It's just that the commits are now in a more logical order instead of the chronological order of the first PR. Some part of this is currently under consideration as SIP-64. Other parts might be proposed as Pre-SIPs in the future.
The order of exposition described in the docs of this PR is different from the planned proposals of SIPs. I concentrate here not on how to sequence details, but instead want to present a vision of what is possible. For instance, the docs in this PR start with Self types and
is
syntax, which have turned out to be controversial and that will probably be proposed only late in the sequence of SIPs.The PR needs a minor release since it adds experimental language imports, which did not exist before. Everything covered is under experimental. Baseline Scala is not affected.