Skip to content

Decide on final syntax for given parameters #7151

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

Closed
odersky opened this issue Sep 1, 2019 · 25 comments
Closed

Decide on final syntax for given parameters #7151

odersky opened this issue Sep 1, 2019 · 25 comments

Comments

@odersky
Copy link
Contributor

odersky commented Sep 1, 2019

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:

  • implicit parameters in methods
  • implicit arguments
  • implicit function literals
  • implicit function types

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

@odersky
Copy link
Contributor Author

odersky commented Sep 1, 2019

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:

  • given is a construct by itself, not a modifier for other definitions, so its syntax focuses on intent over mechanism,
  • different approach to implicit conversions,
  • given arguments are different from normal ones,
  • many differences in details of implicit resolution.

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
Copy link
Member

smarter commented Sep 1, 2019

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
Copy link
Member

smarter commented Sep 1, 2019

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
Copy link
Member

smarter commented Sep 1, 2019

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
Copy link
Contributor

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
Copy link
Member

smarter commented Sep 5, 2019

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:

  • In a method parameter list, given Foo means "an anonymous given of type Foo, and given x: Foo, but in a given instance definition, the equivalents are respectively given as Foo and given x as Foo, this is not symmetrical
  • The syntax of given instances is rich enough that forgetting the as will yield something that parses but which means something completely different, yielding at best confusing error messages as in https://contributors.scala-lang.org/t/proposal-to-add-implied-instances-to-the-language/3070/195?u=smarter
  • There's already a token for separating a term from its type, it's :

My proposal:

  • Instead of:
    given IntOrd as Ord[Int] { ... }
    Use a column:
    given IntOrd: Ord[Int] { ... }
  • Instead of:
    given as Ord[Int] { ... }
    Use nothing, like in a method parameter list, or an implicit function type:
    given Ord[Int] { ... }
  • Instead of:
    given ListOrd[T] as Ord[List[T]] given (ord: Ord[T]) { ... }
    Use a column, and put the given clause before the type (like in classes, defs, etc):
      given OrdList[T](given Ord[T]): Ord[List[T]] { ... }
  • Instead of:
    given [T] as Ord[List[T]] given (ord: Ord[T]) { ... }
    Write:
      given [T](given Ord[T]): Ord[List[T]] { ... }
    (or alternatively forbid anonymous parameterized givens, because when implicit resolution fails, they're going to make for hard-to-decypher compiler error messages)

@soronpo
Copy link
Contributor

soronpo commented Sep 5, 2019

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
Copy link
Contributor

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
Copy link

lavrov commented Sep 6, 2019

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
Copy link
Contributor

LPTK commented Sep 6, 2019

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
Copy link

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
Copy link
Contributor Author

odersky commented Sep 8, 2019

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
Copy link
Member

smarter commented Sep 8, 2019

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
Copy link
Contributor Author

odersky commented Sep 9, 2019

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
Copy link
Contributor

LPTK commented Sep 9, 2019

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) { ... }

odersky added a commit to dotty-staging/dotty that referenced this issue Sep 9, 2019
Following the discussion in scala#7151, choose `:` instead of `as` for given clauses.
@lavrov
Copy link

lavrov commented Sep 9, 2019

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
Copy link
Member

smarter commented Sep 9, 2019

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)

odersky added a commit to dotty-staging/dotty that referenced this issue Sep 9, 2019
Following the discussion in scala#7151, choose `:` instead of `as` for given clauses.
@TomPoczos
Copy link

TomPoczos commented Sep 11, 2019

(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)

odersky added a commit to dotty-staging/dotty that referenced this issue Sep 11, 2019
Following the discussion in scala#7151, choose `:` instead of `as` for given clauses.
@heksesang
Copy link

heksesang commented Sep 12, 2019

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
Copy link
Contributor

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
Copy link

lbkb commented Sep 17, 2019

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
Copy link

TomPoczos commented Sep 17, 2019

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
Copy link
Contributor Author

odersky commented Sep 18, 2019

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.

@odersky odersky closed this as completed Sep 18, 2019
@arturopala
Copy link
Contributor

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
Copy link

Mocuto commented Sep 24, 2019

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.

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

No branches or pull requests