-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Givens without as
#10538
Givens without as
#10538
Conversation
c868640
to
82e7c4e
Compare
Setting aside whether this is a good idea for now, it's worth thinking about how this could be encoded, I assume it would be something like: class some_generated_name extends { ... }
def foo(x: Int): some_generated_name = new some_generated_name The problem with this is: what should class result_foo_Int_extends_T { ... }
def foo(x: Int): result_foo_Int_extends_T = new result_foo_Int_extends_T But then, what happens if I write: val z = foo(1) Is the type of |
@smarter Ah yes, overloading throws a spammer in the works, as always. If we take the |
This is great! I have felt uncomfortable with aspects the |
When developing teaching material for existing anonymous given [A] as Show[List[A]] The word On the other hand, the following syntax can be more easily explained: given [A]: Show[List[A]] By pointing out the similarity to polymorphic def methods, which introduce their type parameters before type ascription. Then |
Not sure if this kind of bikeshedding is welcome here, but how about: given intOrd: Ordering[Int] = new
...
given listOrd[T: Ordering]: Ordering[List[T]] = new
...
given Ordering[Int] = v
...
given [T: Ordering]: Ordering[List[T]] = new
...
given global: ExecutionContext = ForkJoinContext()
given Context = ctx If we make
Alternatively, we could ask for given intOrd: Ordering[Int] = new:
...
given listOrd[T: Ordering]: Ordering[List[T]] = new:
...
given Ordering[Int] = v
...
given [T: Ordering]: Ordering[List[T]] = new:
...
given global: ExecutionContext = ForkJoinContext()
given Context = ctx Honestly to me the Having |
|
@odersky if it's possible to write |
Add tests for them. Also, improve error message if a given instance still has abstract members.
Structural instances (i.e. use `with` instead of `new` for anonymous classes) are not part of this PR.
02d7bd0
to
a5bac26
Compare
The community build still needs to be updated. But I propose to do this in a separate PR after the review, because of the churn. It's basically impossible to do any large scale changes to the CB and then wait for the PR to be reviewed. Things will break very quickly and it takes a lot of effort to keep them up-to-date. |
@LPTK what you describe seems identical to the "target typed constructor
invocations" feature that's landing in C#. I can imagine it being useful
for a wide range of other scenarios as well, not limited to `given`
instance initialization
https://www.thomasclaudiushuber.com/2020/09/08/c-9-0-target-typed-new-expressions/
…-Haoyi
On Tue, 1 Dec 2020 at 12:50 AM, odersky ***@***.***> wrote:
The community build still needs to be updated. But I propose to do this in
a separate PR after the review, because of the churn. It's basically
impossible to do any large scale changes to the CB and then wait for the PR
to be reviewed. Things will break very quickly and it takes a lot of effort
to keep them up-to-date.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#10538 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAHEB7AIYNPMR36GJMA7ZCTSSPEMXANCNFSM4UF6MFVA>
.
|
The currently implemented status is this: trait T { def x: Int }
def f(x: T): T = new { def x = 1 } works, but this one does not: trait T(x: Int)
def f(x: T): T = new(1) But nothing is specified or documented yet. We need to go over it and decide what to do with it. I believe extending it to full target typed constructor applications a la C# would need more discussion and would have to come after 3.0. One issue is it runs somewhat counter to the idea of creator applications. Now there would be two ways to simplify |
But there's another reason why any form of |
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.
LGTM
This is a big improvement and makes the language more regular 👍
The only reservation I have is the introduction of structural givens. On one hand, its succinctness makes it elegant for usages like the following:
given Conversion[Int, String] with
def apply(x: Int) = ""
end
On the other hand, it does not look natural with some usage:
given ops1: IntOps with {} // brings safeMod into scope
The code above is more readable if rewritten as follows (and I guess most users would not mind the duplicate):
given ops1: IntOps = new IntOps
More importantly, it seems to break the regularity of the Scala syntax: all concrete inlinable/reducible definitions are defined with "=". This might complicate learning and reasoning about Scala programs.
Meanwhile, in most usage of structural givens, programmers can still write given T = new T(...) { ... }
for readability. The might be a concern as it introduces two ways of doing the same thing.
infix type +[X <: Int | String, Y <: Int | String] = (X, Y) match { | ||
import scala.annotation.infix | ||
|
||
type +[X <: Int | String, Y <: Int | String] = (X, Y) match { |
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.
infix
is accidentally removed?
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's not needed for symbolic identifiers.
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 we need to remove import scala.annotation.infix
.
/** with Template, with EOL <indent> interpreted */ | ||
def withTemplate(constr: DefDef, parents: List[Tree]): Template = | ||
if in.token != WITHEOL then accept(WITH) | ||
possibleTemplateStart() // consumes a WITHEOL token |
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.
Do we miss else
here?
possibleTemplateStart() // consumes a WITHEOL token | |
else possibleTemplateStart() // consumes a WITHEOL token |
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. possibleTemplateStart
expects a {
or a WITHEOL
.
@@ -131,7 +131,7 @@ than other classes. Here is an example: | |||
trait Vehicle extends reflect.Selectable { | |||
val wheels: Int | |||
} | |||
val i3 = new Vehicle { // i3: Vehicle { val range: Int } | |||
val i3 = Vehicle with { // i3: Vehicle { val range: Int } |
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 change needs to be reverted.
In any case, if |
Once implemented, tested at length, and then found inferior to |
What's wrong with this?
How were abstract givens handled with the previous syntax? |
Changes in a nutshell:
instead of the current syntax:
Syntax
Here is the syntax for given instances:
A given instance starts with the reserved word
given
and an optional signature. The signaturedefines a name and/or parameters for the instance. It is followed by
:
. There are three kindsof given instances:
with
and a template bodythat contains member definitions of the instance.
=
and a right hand side expression.How did we get here?
The previous syntax for givens provided alias givens and structural givens like this.
It's the only instance in the grammar where a construct can be followed by either an
=
and an alias or a template in braces. Everywhere else we distinguish between the two. The main reason for this is what happens if there is neither a=
nor a{
.I.e.
val x: T
is an abstract val, whereasobject x extends T
is an empty objectThat's why I felt that we need a keyword after the name, to emphasize that we get a concrete
given
with an empty body if followed by nothing.But what if we turn around that decision? I.e. what if we take an empty continuation as an abstract given? Several people have presented use cases for abstract givens. While it's true that they can be encoded, the encodings do lead to some duplication, so at first sight it seems like a welcome generalization to support abstract givens. If we do switch that convention, then we do not need a keyword as connective anymore. Arguably
:
is the right choice then.But if we do that there should be something heavier than just
{}
to distinguish between an abstractgiven x: T
and a concrete givengiven x: T{}
, in particular sinceT{}
can be read as a refinement type. Everywhere elsethe choice between abstract and concrete is made clear by the absence or presence of
=
. We can't use=
since we expect a block after an=
, not a template body containing definitions. So we need something else.In this PR, the connective starting a template body is
with
.Ramifications and Speculations
This has another interesting consequence. Taken by itself, can we give a meaning to
T with { defs }
? In fact, this does make sense as an alternative syntax for an anonymous class. If we go down that path, we can truly get rid of all vestiges ofnew
in the syntax (over time, not for 3.0).Taking this further, we can now see the following (approximate) equivalence:
So,
: C with {...}
is approximately: C = C with {...}
, and it avoids the repetition. In fact: C with {}
is more useful than: C = C with {...}
since it keeps any refinements in{...}
in the type. So it's more like a RHS= new {...}
and an inferred result type. In fact it's even better than that, since an anonymous classnew { ... }
is subject to avoidance, but: C with { .. }
creates the class alongside the given instead of in its right hand side. This means that members freshly introducedin
{...}
are visible in the result of the given. To see the difference, consider this code:Now, one intriguing step further is whether we want to allow the same convention for normal defs. I.e
as a slightly more powerful alternative to
or, using
new
with the same expansion as for the
given
. This would give us parameterized objects, or functors in the SML sense,without having to define a class. It also gives a nice way to avoid duplication between return type and RHS while stile having an explicitly defined return type.
Summary
The new design leads to a strong similarity between givens and other definitions. Essentially
given
expands tolazy val
ordef
(we leave it to the system to decide), and the name is optionalThis makes the language more regular. Ity also makes it very easy to switch from explicit definitions to givens and back.
Other benefits:
new
Timeline
The change to the given syntax, including support for abstract givens, would have to be done before 3.0 release. The rest could come later.