scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.85k stars 1.06k forks source link

Decide on final syntax for `given` parameters #7151

Closed odersky closed 5 years ago

odersky commented 5 years ago

It seems we have implicit consensus on given as the new name for implicit instances. It has a lot more support than its alternative delegate. So, let's take that as a given šŸ˜‰

given is a good name for instances, but it does have a potential problem that given parameters and given instances are too easily confused. The example that made this painfully clear for me was in #7056, where we find:

given [T1 <: NonEmptyTuple, TZ <: Tuple] as Aux[T1, Head[T1] *: TZ]
given Aux[Tail[T1], TZ] = new Idnt[T1] {
  type Res = Head[T1] *: TZ
  def (t1: T1) idnt: Res = t1.head *: t1.tail.idnt
}

At first, I could make neither heads nor tails of it and thought that the parser was faulty. Then I realized that the second given is a parameter to the first! Sure, it should have been indented but still... The syntax is dangerously misleading.

One way to fix this is to choose different names for introducing parameters and instances. I.e., go back to delegate. Another way to fix it is to put given under parentheses, thereby using the standard way of expressing parameter dependencies. I.e. it would be

def f(x: T)(given Context): T
f(x)(given ctx)

instead of

def f(x: T) given Context: T
f(x) given ctx

This idea, originally proposed by @smarter, is elaborated in #7150 (docs only, no implementation). One advantage is that it generalizes readily to implicit function types and literals. These would be

(given A) => B
(given x: A) => b

Another advantage is that it makes it possible to have normal parameters after given parameters, something we stopped allowing because the original syntax was so confusing.

A possible downside is in the application of multiple given arguments in one argument list. That would look like

f(given a, b)

instead of

f given (a, b)

The first syntax takes some getting used to, I think.

Another possible option is to keep given for instance definitions, but use something else for implicit parameters. where was suggested by @milessabin. But any solution has to work in all of the following cases:

where works OK for the first two, but not for the last two.

odersky commented 5 years ago

I realize that going back to given under parentheses looks more like the old implicit parameters. Are we turning in circles here, just replacing implicit by given? I think not. There are a lot of differences between given and implicit:

Furthermore, given has the right connotation for both instance definitions and parameters, whereas implicit only works well for implicit parameters, is barely adequate for implicit definitions (when I write implicit object o { ... } it's just as explicit as a normal one!), and would be completely wrong for explicitly given implicit arguments. So given is altogether a much better choice than implicit.

smarter commented 5 years ago

Another problem with the current syntax is that:

def foo given (x: A, y: B) = ...
def foo given (A, B) = ...

Mean different things, the first takes two implicits of type A and B, the second takes one implicit of type (A, B), so it's easy to make the mistake when changing code: https://twitter.com/travisbrown/status/1152530738144845830

smarter commented 5 years ago

Contrast with the parens-around-given approach:

def foo(given x: A, y: B) = ...
def foo(given A, B) = ...

Both of these mean the same thing, one has to write def foo(given (A, B)) to get an implicit of a tuple type.

smarter commented 5 years ago

Another possible option is to keep given for instance definitions, but use something else for implicit parameters. where was suggested by @milessabin. But any solution has to work in all of the following cases: [...]

At the risk of restarting the infinite syntax wars: I think inject could work for all of those

liufengyun commented 5 years ago

Another merit of putting given in parenthesis: it's much easier to see what's the return type of a method.

Compare

  def dyni given QuoteContext: PV[Int] => Expr[Int] = dyn[Int]

with

  def dyni(given QuoteContext): PV[Int] => Expr[Int] = dyn[Int]

For myself, the return type of the second is immediately obvious, but my brain spins a while until I figure out the return type of the first.

smarter commented 5 years ago

While we're on the subject of given instances, I think it's worth reconsidering the use of as, while given as Foo looks fine by itself, I think that it doesn't fit well with the rest of the language:

My proposal:

soronpo commented 5 years ago

Please consider changing given to extension (as a soft keyword) for extension methods.

extension ListOps[T](xs: List[T]) {
  def second: T = xs.tail.head
  def third: T = xs.tail.tail.head
}
liufengyun commented 5 years ago

Regarding the syntax for import, currently it looks unnatural and a little verbose:

import A.{ given as TC }

For brainstorming, what about:

import A.{ the[TC] }

The downside of the proposal is that the is not a keyword.

lavrov commented 5 years ago

Giving the type of implicit instance after as is a bit confusing for me because it seems that as should introduce identifier instead as it does in SQL. šŸ‘ to @smarter idea.

LPTK commented 5 years ago

it seems that as should introduce identifier instead as it does in SQL

Also in OCaml, where it's used like Scala's @ in patterns!

ryanstull commented 5 years ago

Yeah I like the idea of replacing as with : as that's how the type of a term is already denoted. I think @smarter's proposal feels more consistent with the rest of the language

odersky commented 5 years ago

The idea to regularize the language by choosing : over as is attractive. But it also causes a problem: In

given ListOrd[T](given X: T): Ord[List[T]] { 
   ...
}

it is very hard to see whether what we define is an anonymous given for a class ListOrd or a named given for a class Ord. In fact, we have to scan ahead a long way until we find the : which clarifies what it is. Long scanning ahead is a problem for humans and for the parser.

The situation is not comparable with given parameters where we have to distinguish (given C) and (given x: C) since the only possible item in front of a colon is a simple identifier. That is easy to parse for both humans and compilers.

smarter commented 5 years ago

it is very hard to see whether what we define is an anonymous given for a class ListOrd In fact, we have to scan ahead a long way until we find the : which clarifies what it is.

I don't think we need to scan that far, as soon as we see the the paren introducing (given ..., we know that this isn't a type so it's not anonymous, furthermore editors are likely to syntax highlight : specially, and just seeing : on the line should be enough to realize it's not anonymous.

This could also be reinforced by encouraging named givens to always start with a lower-case letter:

given listOrd[T](given X: T): Ord[List[T]] { 
   ...
}

After all, the given generates both a class and a def, and the important part is that calling listOrd(foo) will return something of type Ord[List[T]], not that this is implemented using a class.

odersky commented 5 years ago

I don't think we need to scan that far, as soon as we see the the paren introducing (given ..., we know that this isn't a type so it's not anonymous

No, in fact the implemented type can be passed a given argument since what's on the right of the : is a class constructor, not a type. So (given ...) is still ambiguous.

This could also be reinforced by encouraging named givens to always start with a lower-case letter

That's a good convention. So that solves the human readability problem. The necessary lookahead in the parser is a solvable problem. That convinces me to go with : instead of as.

Follow-on question: Should we also use : for extends, like C#? That would make the language even more regular.

LPTK commented 5 years ago

So that solves the human readability problem.

I'm not so sure about that. As long as the convention is not enforced, some people will stray away from it, and it will make for very confusing code to read for others.

The fact that in a definition, the given keyword may be followed either by an identifier or a type seems really weird and inconsistent. So far, Scala has been pretty consistent in cleanly separating term and type syntaxes. This would steer Scala towards the ugly parsing ambiguities that plague languages like C++ because they didn't enforce a clean term/type separation.

To me, the obvious solution is to require an underscore in place of the name for anonymous instances. (This could be generalized for all other anonymous definitions.)

Then distinguishing one from the other at a glance becomes easier:

given ListOrd[T](given Ord[T]): Ord[List[T]] { ... }

given _: ListOrd[Int](given IntOrd) { ... }

But as shown above, there's still the strange false symmetry between (given T) parameters where T is a type and (given A) arguments where A is a term.

Requiring underscores there too is possible and would make sense, but may be deemed too cumbersome:

given ListOrd[T](given _: Ord[T]): Ord[List[T]] { ... }

given _: ListOrd[Int](given IntOrd) { ... }
lavrov commented 5 years ago
given Ord[Int] { ... }

I believe this form reads the best i.e. the form where instance type comes right after given. Is it possible to make it look similar for other cases as well? Something like:

given Ord[List[T]] where [T](given Ord[T]) as listOrd { ... }

where least important part such as an identifier comes last.

smarter commented 5 years ago

No, in fact the implemented type can be passed a given argument since what's on the right of the : is a class constructor, not a type. So (given ...) is still ambiguous.

Good point. In fact, I hadn't really considered that we have both given aliases and given instances in my proposal above, I fear the subtle syntactic differences between them will lead to puzzlers, e.g.:

Question: what's the difference between these two given definitions?

class Foo {
  println("hi")
}

given Foo =
  new Foo

given Foo {
  new Foo
}

Answer: The first creates a def of type Foo whose right-hand-side is new Foo, the second creates a subclass of Foo whose primary constructor contains a call to new Foo, which doesn't really make sense, but it's an easy mistake to make, and the result might still sort of work! (and indentation-based syntax would make the delta between the two syntaxes even shorter). In a way, this is worse than the issues that lead to the removal of procedure syntax.

It seems that the only way to prevent this sort of mistakes is to make the syntax more explicit and not just rely on a few symbolic tokens to convey intent.

Since the documentation already talks about given aliases and given instances, we could reuse that vocabulary in the code too:

given alias Foo = ...
given alias x: Foo = ...
given instance Foo { ... }
given instance x: Foo { ... }

If this is felt to be too verbose, another alternative would be to use given for given aliases and given instance for given instances, that way the more heavy-weight alternative gets a longer syntax which seems reasonable:

given Foo = ...
given x: Foo = ...
given instance Foo { ... }
given instance x: Foo { ... }

Another way to make the distinction clearer would be to replace as by : for given aliases but replace it by extends for given instances:

given Foo = ...
given x: Foo = ...
given extends Foo { ... }
given x extends Foo { ... }

This makes it easy to associate given instances with the concept of class definition (if we keep extends for class that is)

TomPoczos commented 5 years ago

(edit: this was written under the impression given is used as adjective currently, if that doesn't hold it might sound slightly differently than intended)

Didn't the originally proposed implied/given syntax work well? My impression was that it did. Just replace implied with any synonymous adjective other than 'given'? Or keep implied?

Assumed, default, provided, implied, present, supplied, latent; doesn't really matter, no point bikeshedding over the specific word too much, so long as it is not 'given', to avoid the confusion.

Not much would change either, neither compared to the original nor the current proposal.

(Also I get the desire to have a noun here but last time when I tried to find a suitable one... I can't think of any specific ones that would fit. OTOH, I got half a dozen or so more or less fitting adjectives under a minute)

heksesang commented 5 years ago

What about using a verb for declaration of the instances? In this line you essentially give an instance of a type, and you expect something to do this, a given.

give ListOrd[T] as Ord[List{T]] given Ord[T] { ... }

You can of course replace it with another word, provide and provided for example, as long as the words are duals and a verb that describes the act of creating an instance and a noun that describes that the instance is expected to already exist.

provide ListOrd[T] as Ord[List{T]] provided Ord[T] { ... }

I find my proposed syntax a lot more readable, and so far I feel like I have seen too many suggestions not addressing the biggest issue with implicit: the fact that the keyword had multiple meanings.

arturopala commented 5 years ago

I strongly agree with @heksesang that Scala should have a pair of keywords here, one for providing, one for consuming implicit value or type, like give/given. I would wish to be able to read Scala code directly, without ambiguities, without solving keyword puzzles.

lbkb commented 5 years ago

Agreed. That given [T](given Ord[T]): Ord[List[T]] { ... } looks awkward. With separate keywords it looks clearer:

implicit listOrd[T] (implied Ord[T]): Ord[List[T]] { ... }

foo(x)(explicit y)
give listOrd[T] (given Ord[T]): Ord[List[T]] { ... }

foo(x)(give y)
canonize listOrd[T] (canonical Ord[T]): Ord[List[T]] { ... }

foo(x)(canonize y)
TomPoczos commented 5 years ago

I wasn't really a fan of using a verb, but these matching (imperative) verb / adjective pairs look the most intuitive to me so far.

Also works with assume/assumed on top of the examples already mentioned, and imply/implied might be considered instead of implicit/implied.

Easier to describe and discuss than I thought too, I guess if you provide an instance you can just talk about it as the provided instance.

Edit: Of the suggestions mentioned provide/provided would work best for me, just feels natural.

odersky commented 5 years ago

We have seen widespread consensus for the scheme that was merged in #7210. Widespread in the sense that everyone who has looked at it so far liked it, which is quite differences from experience with previous versions.

arturopala commented 5 years ago

I have had detailed look at a current state of given in the docs and still have a feeling that using the same given keyword for both sides, to bring and to summon, is extremely confusing. Has given became new implicit, just shorter?

Mocuto commented 5 years ago

Why not something like: let ListOrd[T] as Ord[List{T]] given Ord[T] { ... } I definitely think a cleaner solution than reusing given and dropping the infix notation is to use a different keyword for implied instances.