-
Notifications
You must be signed in to change notification settings - Fork 28
SIP-64: Improve the Syntax of Context Bounds and Givens #81
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
Conversation
content/typeclasses-syntax.md
Outdated
That's generally considered too painful to write and read, hence people usually adopt one of two alternatives. Either, eschew context bounds and switch to using clauses: | ||
```scala | ||
def reduce[A](xs: List[A])(using m: Monoid[A]): A = | ||
xs.foldLeft(m)(_ `combine` _) |
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.
m.unit
content/typeclasses-syntax.md
Outdated
Since we don't have a name for the `Monoid` instance of `A`, we need to resort to `summon` in the body of `reduce`: | ||
```scala | ||
def reduce[A : Monoid](xs: List[A]): A = | ||
xs.foldLeft(summon Monoid[A])(_ `combine` _) |
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.
summon[Monoid[A]].unit
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.
To me, these are two separate changes that can be considered completely independently:
- Context bound changes
- Given syntax changes
I think it would be valuable to discuss, implement and approve these two independently.
I think something is off in this branch. The Named Tuples commits (from I imagine this was intended to only start at |
trait: | ||
def showMax[X : {Ordering, Show}](x: X, y: X): String | ||
class B extends A: | ||
def showMax[X : {Ordering as ordering, Show as show}](x: X, y: X): String = |
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 we should add a discussion (apologies if I missed it) of the fact that some using/givens don't have a single parameter to dispatch on. For such cases we would still employ the using variant. To me I'd like to see a case that we could completely kill the using variant. To do that, it would be good to have a catalog of examples that don't fit. For instance, does Builder/CanBuildFrom work with this approach? From cats, MonadError won't work with this, I don't think. In any case taking a few such examples like that would strengthen this.
267b8ff
to
fe41123
Compare
@JD557 yes, indeed. I force pushed as an independent separate branch, incorporating fixes to the two typos pointed out by The proposal could be split further into two or three independent areas, but there are also connections between the parts:
So one logical progression could be (1) deferred givens replacing abstract givens (2) new given syntax (3) context bound changes. But the motivation why the new given syntax is harmonious comes in part from the fact that it is in agreement with names for context bounds.
I don't think that's possible or desirable. In my world view type classes are a kind of types for types. That means they can only refer to a single type. Multi-parameter type classes are really constraints passed as context. So "multi-parameter type class" is already a misnomer. The name was invented in Haskell because Haskell does not have a general context passing mechanism, all has to be force-fitted into the typeclass paradigm. And of course there are also bits of context that don't constrain any type parameters. So it seems natural to keep using clauses for these cases, and reserve context bounds for true (that is, single-parameter) type classes. |
content/typeclasses-syntax.md
Outdated
are time sensitive since they affect existing syntax that was introduced in 3.0, so it's better to make the change at a time when not that much code using the new syntax is written yet. | ||
|
||
|
||
1. Named tuples are a convenient lightweight way to return multiple results from a function. But the absence of names obscures their meaning, and makes decomposition with _1, _2 ugly and hard to read. The existing alternative is to define a class instead. This does name fields, but is more heavy-weight, both in terms of notation and generated bytecode. Named tuples give the same convenience of definition as regular tuples at far better readability. |
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.
Is this from this SIP?
It's unclear how named tuples are connected with context bounds and givens.
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.
Oops, no that was an oversight. Deleted.
content/typeclasses-syntax.md
Outdated
|
||
- An optional _precondition_, which introduces type parameters and/or using clauses and which ends in `=>`, | ||
- the implemented _type_, | ||
- an optional name binding using `as`, |
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 the meta-rule here is:
-
<prefix> as <name>
with the name on the right is used where the name is optional and usually elided (import renames, pattern-match name bindings, given names) -
<keyword> <name> <suffix>
style with the name on the left is used where the name is mandatory and important (val
,def
,object
,class
, etc.)
Some random thoughts about the parts of the proposal:
def lexicographicOrd[T](using Ord[T]): Ord[List[T]] = ??? through given lexicographicOrd[T](using Ord[T]): Ord[List[T]] = ??? to given lexicographicOrd[T](using Ord[T]): Ord[List[T]] with That makes the last syntax intuitive, even if it is awkward. The proposal doesn't mention alias givens at all, leaving a question about whether it intends to introduce a huge inconsistency between ordinary given instances definitions and alias given definitions.
|
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.
Overall 1, 2, 3 and 5 look good to me.
I have serious concerns about 4.
I am sympathetic to 7 (6?) but it's not clear that a library could migrate off of abstract givens without breaking its own binary API.
content/typeclasses-syntax.md
Outdated
**Alternative:** It was suggested that we use a modifier for a deferred given instead of a `= deferred`. Something like `deferred given C[T]`. But a modifier does not suggest the concept that a deferred given will be implemented automatically in subclasses unless an explicit definition is written. In a sense, we can see `= deferred` as the invocation of a magic macro that is provided by the compiler. So from a user's point of view a given with `deferred` right hand side is not abstract. | ||
It is a concrete definition where the compiler will provide the correct implementation. |
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 don't see how this can be viewed as a magic macro. Magic or not, a macro is expanded at the call site of the macro. Not somewhere else.
No, this is definitely not a concrete member with a magical body. It is an abstract member that happens to receive some concrete implementation automatically in subclasses.
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.
A non-magic macro is expanded at the use site. A magic macro can avoid that restriction ;-)
The point is, for a deferred given you know it will be (attempted to be) implemented without requiring special provisions for implementers. That's why it is not abstract.
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 am still completely baffled by this line of thought. Magic things are fine when they do things that normal things cannot do, but still where normal things can do them. Example of good magic things:
- Primitives
uninitialized
compiletime.ops
The proposed deferred
has non-local consequences that go far beyond what we can explain as a macro, even magic. On the contrary, it completely matches what modifiers have the power to do: final
has the power to non-locally prevent overriding, for example. deferred
as a modifier would be completely in line with other modifiers.
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.
Taking my reasoning further, there are other practical consequences here.
Scaladoc
As a user of a trait containing a deferred given, when I extend it, it is completely different for me if that it is a deferred given or a concrete given with any other body. That means Scaladoc must show the deferred aspect of it. This is not something Scaladoc does for things after =
: the part after the =
is an implementation detail, not an API. (Unless the thing is an inline
itself, but being a macro is completely different than calling a macro.)
Implementation under separate compilation
Likewise, and for the same reason, the compiler also needs to look at the deferred property of things coming from separate compilation. So for practical reasons it also makes no sense for it to be on the implementation side of the =
. It is clearly part of the ABI contract (compare ABI here with API in my Scaladoc point).
content/typeclasses-syntax.md
Outdated
- Simplification of the language since a feature is dropped | ||
- Eliminate non-obvious and misleading syntax. | ||
|
||
The only downside is that deferred givens are restricted to be used in traits, whereas abstract givens are also allowed in abstract classes. But I would be surprised if actual code relied on that difference, and such code could in any case be easily rewritten to accommodate the restriction. |
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.
Could you please explain how such code can be "easily rewritten"? I don't think it is easy, or even perhaps possible, to rewrite such code in a way that preserves the binary API of an open class/trait that contains an abstract given.
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.
No it would break the binary API. The thing is, I am not sure there is any use case in the wild where abstract givens are used in abstract classes. People usually reach for traits anyway.
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.
Then, as I mentioned before offline (but I can't find a written trace online), it is simply not acceptable to ever get rid of abstract givens. If there is a library that uses one today, it will never be able to upgrade its compiler version, no matter what source rewritings it attempts to do.
This is a big no-no. Removing them would not only break backward source compat (which we can do), it would break our promises of binary compatibility due to second-order effects.
We can deprecate them, with the usual options to silence the deprecation, but we can never remove them.
content/typeclasses-syntax.md
Outdated
|
||
- A `given` clause consists of the following elements: | ||
|
||
- An optional _precondition_, which introduces type parameters and/or using clauses and which ends in `=>`, |
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.
Could you show how using clauses would look like in this context? What about a combination of type parameters and using clauses? All the examples use type parameters only.
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.
given [T](using Ord[T]) => Ord[List[T]]:
...
```scala | ||
given T = deferred | ||
``` | ||
`deferred` is a new method in the `scala.compiletime` package, which can appear only as the right hand side of a given defined in a trait. Any class implementing that trait will provide an implementation of this given. If a definition is not provided explicitly, it will be synthesized by searching for a given of type `T` in the scope of the inheriting class. Specifically, the scope in which this given will be searched is the environment of that class augmented by its parameters but not containing its members (since that would lead to recursive resolutions). If an implementation _is_ provided explicitly, it counts as an override of a concrete definition and needs 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.
I find it awkward that we introduce behavior that is very different between traits and classes. There is no precedent for anything like this in the language. In fact, Scala 3 brought classes and traits closer to each other by allowing constructor parameters in traits.
What if I already have the concrete interpretation in trait
? What if I don't have the concrete interpretation in an abstract class?
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 find it awkward that we introduce behavior that is very different between traits and classes. There is no precedent for anything like this in the language.
There is precedent. Trait parameters are resolved in the next enclosing class.
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.
OK. I still think it is weird but it is acceptable.
```scala | ||
trait Sorted: | ||
type Element | ||
given Ord[Element] = compiletime.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.
Here is my main concern with this proposal. I think this expansion is dangerous and will create compatibility issues. Not backward incompatibility at the language, but intrinsic, synchronous incompatibility between libraries.
The core issue is that this desugaring introduces "nameless" members whose name actually matters a lot. There is no precedent for that in the language.
Consider for example
trait Babar[T]
given Babar[Int] with {}
given Babar[String] with {}
trait BaseA[A] {
given Babar[A] = deferred
}
trait BaseB[A] {
given Babar[A] = deferred
}
class Child extends BaseA[Int] with BaseB[String]
Now you need to implement two given_Babar_A
that have nothing in common but that happen to share a generated name.
You could argue that this is already problematic with given
definitions today, but at least they have an explicit definition. Once we automatically generate given Ord[Element]
from type Element : Ord
, and then given_Ord_Element
from that, there is a multi-step, non-obvious relationship between an innocuous type
definition and auto-generated names that clash.
Another example would be
type Foo : {package1.Bar, package2.Bar}
which would immediately result in clashing members. Generalize it to type Foo : package1.Bar
in one super trait and type Foo : package2.Bar
in another supertrait, and things becomes even more obscure.
There are so many ways that such a scheme is going to go wrong. At the level of the compilation scheme, we're approaching the amount of danger of value classes. Value classes also expose issues that arise in similar situations: where a generic in a superclass cannot be instantiated to some value classes because their bridges double-clash.
Since the core issue is that a generated name acquires a strong semantic meaning, I think we can fix this by disallowing anonymous things here. We could demand that deferred givens have a name, and that includes demanding them for context bounds on type members.
Even then, some issues remain: what is the visibility of the generated member? Does it match the visibility of the type member? What if the type member is package[something]
but the implementing class is outside something
?
Overall, this proposal introduces for the first time adding invisible members to open traits that actually matter in subtraits and subclasses. We have no precedent for that. There are a zillion issues that could arise from such a new situation, and they need to be explored much more carefully than what the current SIP text suggests.
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.
Now you need to implement two given_Babar_A that have nothing in common but that happen to share a generated name.
The attempt to provide synthesized implementations should fail with a double definition error in this case. The user can always solve the problem by defining a single given Babar[A]
in Child
that implements both inherited deferred givens. So there's nothing very new or problematic about it, as far as I can see.
content/typeclasses-syntax.md
Outdated
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. | ||
|
||
|
||
### 7. Abolish Abstract Givens |
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.
Should this be 6?
given Ord[String]: | ||
def compare(x: String, y: String) = ... | ||
|
||
given [A : Ord] => Ord[List[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.
What does the arrow mean here?
content/typeclasses-syntax.md
Outdated
given Ord[String] as stringOrd: | ||
def compare(x: String, y: String) = ... | ||
|
||
given [A : Ord] => Ord[List[A]] as listOrd: |
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.
What does the arrow mean here and how does the as
bind to it?
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.
as
binds to the whole given clause. The =>
means conditional. If there is an [A: Ord]
then here is an Ord[List[A]]
. It's role is similar to the arrow in pattern matching. You could also see it as a given that defines a function [A : Ord]
to Ord[List[A]]
. The two interpretations are the same, so it means that all the usual interpretations of =>
are applicable and they coincide.
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.
Currently the arrow can be used and then it means something different:
Welcome to Scala 3.4.1 (17.0.6, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> given [T]:(T => String) = x => x.toString
def given_T_to_String[T]: T => String
The first time I saw the new arrow syntax in this thread, I thought it was a given for FunctionN, so there could perhaps be some confusion. How should we relate the new syntax to givens for functions? Have you considered alternatives?
@Kordyjan Given clauses are usually written without a name, and that's mostly where the old syntax is weird.
|
This proposal is up for the next SIP meeting on May 24th which is soon and I think we should prepare the meeting discussions here. In general I think the ambition of this SIP is great. I find the first parts of the new syntax and expansion schemes for naming context bounds, multiple context bounds, context bounds for type members reasonable and with support from the compiler to rewrite old syntax and a long grace period before removal I think it is doable. However, the controversial/"risky" part of this proposal is the change of syntax for givens, and the main concern is language stability versus language improvement. Or in other words: is the improvement to givens worth the costs of migration and potential risk of decreased willingness to go for Scala Next? @odersky It would be great if the below issues can be clarified in the proposal or in this comment thread, as input to the meeting:
Assuming something like (or similar): case class MyType(i: Int)
case class MyGenType[A](a: A)
trait MyOldTypeClass[T]:
...
trait MyNewTypeClass:
type Self
...
I think it would be great if you include a summary overview for the proposed new given syntax, perhaps like this (or similar) in the proposal:
As there are many types of givens, some available here, I think it is valuable if there is a such a systematic walk-through of all combinations so we don't miss any feature interaction and also can get an overview of how big the syntax changes are. In the current proposal only a few of all possible kinds of givens are shown in the new syntax and it is not clear to me if more variants are impacted. I think the new arrow syntax using |
Another question I have:
Can deferred givens be allowed also in abstract classes? If no, why not? |
It would complicate things a bit and we would lose flexibility. We want to have a strict separation of classes/traits where deferred givens are defined and where they are implemented. If deferred givens can be defined in abstract classes, then they would in turn not be implemented in abstract classes. But we sometimes might want to implement deferred givens or context bounds in classes that are still missing the implementations of some members. We also have precedent for this split: trait parameters. These are defined in traits and the corresponding arguments must appear in the first implementing class, where it does not matter whether that class is abstract or not. |
Co-authored-by: Jamie Thompson <[email protected]>
The discussion on contributors gave a clear preference for
I propose to change the SIP to reflect these preferences as the standard proposal and to change the implementation accordingly. Are there any objections to this? |
I am happy with it though I suspect it is not the better solution for most people due to lesser familiarity with curried function syntax than with method syntax. Because the In given myOrdList: [A] => Ord[A] => Ord[List[A]]:
def compare... what is that first If we don't want to do anything about this, the discussion with |
Edit: ignore - I proposed to drop the initial colon after name, but I think there are too many ambiguities |
How would the syntax look like if one wants to name (one of the) dependencies? A given listOrd: [A] => (aOrd: Ord[A]) => Ord[List[A]]:
... or B given listOrd[A](using aOrd: Ord[A]): Ord[List[A]]:
... I'm asking, because A feels a bit foreign/alien to me, not like anything in Scala. On the other hand, B looks fine IMHO, it's very similar to a method definition which even Scala newbies are comfortable with. Please disregard my comment if it's too bike-sheddy for too late. But I was curious, what's the latest decision. |
|
||
For instance, type and val above would expand to | ||
```scala | ||
type Comparer = [X] => (x: X, y: X) => Ord[X] ?=> Boolean |
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.
is this right? to me, [X: Ord] => (X, X) => Boolean
should expand to [X] => Ord[X] ?=> (X, X) => Boolean
. Why is the context function getting interwoven in the middle of (X, X) => Boolean
?
The proposal still has examples like: given [A: Ord] => Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ??? to me, a very important concept in programming is the substitution principle. When I look at this, I want to be able to leverage substitution: // this type should be well defined as [A] => Ord[A] ?=> Ord[List[A]]
type OrdList = [A: Ord] => Ord[List[A]]
// and we should be able to use it in a given:
given OrdList:
def compare(x: List[A], y: List[A]) = ...
// or with a value:
val myOrder: [A] => Ord[A] ?=> Ord[List[A]] = ...
given OrdList:
myOrder Are those substitutions valid? |
It's (A)
And yes, that's a legal function type in Scala. It's already used as the syntax for a dependent function type such as (x: C) => x.Elem |
They are not. The arrow really means implication. The analogy with function types serves as a help to remember the syntax, not to be taken literally. I argue this is not a problem since nobody should want to define givens for functions, that's generally a very dubious thing to do. If you really want to implement a give instance of a function type, you can use the type alias as you have shown, or put the type in parentheses. |
In my opinion, defining a given context function seems pretty natural. In fact, that is what we are doing with |
This proposal contains the revised syntax for givens which was discussed here and on contributors. # Conflicts: # content/typeclasses-syntax.md
Yes, that's certainly the inspiration. But the fine print does not work out. Given parameters can carry modifiers or annotations but function parameters can't. That's why I was careful to write "it should look like a function type", not "it should be a function type". Still, I think the analogy with function types is useful for learning and intuition. Most people won't even notice the details where they differ. |
@Ichoran, all: I did a major update of the proposal which now puts the revised syntax first. |
Hmmm. Welcome to Scala 3.4.2 (21.0.3, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> final given f: (Int => Int) = x => x + 1
lazy val f: Int => Int
scala> @unchecked given f: (Int => Int) = x => x + 1
lazy val f: Int => Int
scala> inline def z(using inline f: Int => Int) = f(0)
def z(using f: Int => Int): Int
scala> def z(using @unchecked f: Int => Int) = f(0)
def z(using f: Int => Int): Int
scala> z
val res0: Int = 1
|
@bjornregnell I was referring to parameters not the given definitions themselves. For instance this is OK: given (@unchecked x: T) => U = ... But this is not: val v: (@unchecked x: T) => U = ... Here, |
Aha. Thanks for clarification. @odersky So then I guess it would be an opportunity, when given function values are wanted, to first make a normal def (with all the possible glory of modifiers and annotations etc) and then use it as an eta-expanded value in the body of an alias given like so: Welcome to Scala 3.4.2 (17.0.8.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> inline transparent def f(@unchecked inline x: Int) = x
def f(x: Int): Int
scala> given (Int => Int) = f
lazy val given_Int_to_Int: Int => Int
scala> def z(using f: Int => Int) = f(0)
def z(using f: Int => Int): Int
scala> z
val res0: Int = 0
|
@bjornregnell Yes, that's what I would recommend in this case as well. |
1. A simple typeclass instance, such as `Ord[Int]`. | ||
2. A parameterized type class instance, such as `Ord` for lists. | ||
3. A type class instance with an explicit context parameter. | ||
4. A type class instance with a named eexplicit context parameter. |
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.
typo: "eexplicit" -> explicit
This SIP is up for vote on Friday Sep 27th. As one of the assignees I hereby state my recommendation to the Committee: After discussions and feedback here on Contributors and also updates to the proposal accordingly, I hereby recommend to vote |
I recommend to vote yes as well. |
No description provided.