Skip to content

Implement given/with syntax #8017

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

Merged
merged 22 commits into from
Jan 23, 2020
Merged

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jan 16, 2020

This is an elaboration and full implementation of the docs in #7973. Instead of fiddling with the instance keyword (witness or default) we change the parameter syntax by going back to with, which we had before given.

Using given for parameters has several potential problems:

  • Many commenters felt that this was an over-use of given with too many similarities to the current status where implicit is used for everything.
  • Parameters in given instances were particularly burdensome. This was fixed in Better syntax for conditional given instances #7788, but the fix introduces problems on its own. First, it introduces another irregularity. Second, commenters felt the syntax was confusing because it looked too much like function types.

To start with browsing:

https://github.com/dotty-staging/dotty/blob/change-given-with/docs/docs/reference/contextual/motivation-new.md

@odersky odersky changed the title Retract conditional given syntax Implement given/with syntax Jan 18, 2020
@odersky
Copy link
Contributor Author

odersky commented Jan 18, 2020

A note on indentation: Using with for context parameters means we cannot possibly use it as well for starting an indented definition block for classes and similar. I had some doubts about this usage of with anyway as it was a bit verbose. So this PR replaces with with :. For instance, it's now

object Foo:
  def bar() = 1

instead of

object Foo with
  def bar() = 1

This does not mean we go back to the state where a : at the end of a line would always imply braces. That extended system is still available only under -Yindent-colons. But without that option,: at end of line is only interpreted at the points where a template body or similar starts. Note that this does not introduce an ambiguity with type ascriptions, since no type ascriptions are allowed at these points. https://github.com/dotty-staging/dotty/blob/change-given-with/docs/docs/reference/other-new-features/indentation-new.md explains the details.

I believe : works nicely in this new role. For instance, here's a test that exercises it heavily.

https://github.com/dotty-staging/dotty/blob/change-given-with/tests/pos/reference/delegates.scala

@arturopala
Copy link
Contributor

I did read the provided example test several times and each time feel there is something weird in the proposed syntax:

  • some keywords are nouns (extension), some prepositions (given), I see no clear reason for that
  • I would expect keyword as to set an alias or name, but here, surprise, it sets a resulting type
  • Keyword with does not sound like a condition to me, using when or similar would make it sound better
  • Keyword extension on does a good job, clearly communicates the intent and reads well
  • Similarly extension method syntax is easy to read

I like and use Scala, and would love it to have the most clear and plain syntax as possible, one everyone tech-savy can read and grasp without searching the internet, pls do not compromise on this!

@soronpo
Copy link
Contributor

soronpo commented Jan 19, 2020

It's now

given _
given T

Thta way it's more regular: A given selector is always followed by something: Either
a wildcard or a type.

Regarding the given import syntax, personally I would much rather have:
import given foo._ to import all including the "givens"
import foo._ to import all without "givens"
I'm rather annoyed by the import foo.{_, given _} syntax.

Alias givens can be anonymous, e.g.
```scala
given as Position = enclosingTree.position
given with outer: Context as Context = outer.withOwner(currentOwner)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is with x: T allowed without parentheses, or should it be given with (outer: Context) as Context = ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. It needs to come in parens.

@jdegoes
Copy link

jdegoes commented Jan 20, 2020

I was a proponent of splitting up given into two keywords (I suggested provide / given, e.g. provide Ord[List[T]] given Ord[T]), and I think given / with works about as well as any other choices, so huge 👍 on this from me.

And while subjective, I also think the move away from with as the keyword introducing a code block is more aesthetically pleasing, and more inline with other whitespace-sensitive grammars.

The only thing I'm not quite sold on is as. In the context of SQL, as means to give a name to something, which I believe is the most natural way to use it in English. In C#, it casts to a different type. In FP libraries, it maps to a constant value.

I would suggest the following tweak from this:

given listOrd[T] with (ord: Ord[T]) as Ord[List[T]]

...into this:

given[T] Ord[List[T]] with (ord: Ord[T]) as listOrd

which harmonizes as with its meaning in SQL (probably the most common meaning), and biases toward unnamed givens, which I think is an extremely good "best practice".

In the common case, the above would be written as:

given[T] Ord[List[T]] with Ord[T]

which is quite pleasant. Almost as pleasant as, provide [T] Ord[List[T]] given Ord[T]. 😉

The fact that there are different ways to introduce let bindings is a little unsatisfying. But again could be viewed as a feature if the idea is to bias toward unnamed givens, which I think is the correct pedagogical choice (named givens being reserved for advanced users).

@smarter
Copy link
Member

smarter commented Jan 20, 2020

given[T] Ord[List[T]] with (ord: Ord[T]) as listOrd

The problem with a syntax like that is that it looks backward when the result type depends on a parameter:

given[T] Foo[x.A] with (x: T) as foo

it also would be backward from the way with ... is used in regular defs with implicit parameters.

@jdegoes
Copy link

jdegoes commented Jan 20, 2020

@smarter Haskell "solves" that problem by essentially swapping the order, like:

with[T] (ord: Ord[T]) given Ord[List[T]] as listOrd

But it has the same problem as Haskell, which is that to parse out the instance type with your eyes, you have to look past the constraints, which can be arbitrarily long.

OTOH you could use convention to format it like so:

with[T] (ord: Ord[T]) 
  given Ord[List[T]] as listOrd

such that additional constraints don't change where you look for the given.

Maybe as isn't even needed, you could just provide the given a name like the with, using type ascription syntax:

with[T] (ord: Ord[T]) 
  given (listOrd: Ord[List[T]])

Then at the risk of bike-shedding too much, change with to given, and given to implies:

given[T] (ord: Ord[T]) 
  implies (listOrd: Ord[List[T]])

Or possibly provide:

given[T] (ord: Ord[T]) 
  provide (listOrd: Ord[List[T]])

@odersky
Copy link
Contributor Author

odersky commented Jan 20, 2020

@jdegoes Regarding as, I answered on the contributors thread, copied here:

I’d read

given ord as Ord[T] { ... }

“given ord as the Ord[T] instance where …”

If the “ord as” part is missing it’s just “given the Ord[T] instance where…”.

By that interpretation, the chosen syntax corresponds exactly to spelling it out loud.

@jdegoes
Copy link

jdegoes commented Jan 20, 2020

@odersky I think that's teachable, which is the most important concern for me. 👍

@sideeffffect
Copy link
Contributor

sideeffffect commented Jan 20, 2020

this PR replaces with with :

has = been considered? It would be more aligned with other parts of the language, like we don't write def sum(x, y) with x + y or def sum(x, y): x + y, but def sum(x, y) = x + y. For example, a class definition would look like this

class C(x: Int, y: Int) = A(y) with
  def f = x * y

It also reads nicely: class C of x and y is class A of y with added method f

@@ -69,7 +69,7 @@ maximum(xs).with(descending.with(listOrd.with(intOrd)))

## Multiple With Clauses

There can be several with clauses in a definition and with clauses can be freely interspersed with normal parameters. Example:
There can be several with clauses in a definition. Example:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It think it is better to write "There can be several with clauses"

@@ -88,6 +88,17 @@ f(global).with(ctx).with(sym, kind)
```
But `f(global).with(sym, kind)` would give a type error.

With clauses can be freely interspersed with normal parameters, but a normal parameter clause cannot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With clauses

@@ -88,6 +88,17 @@ f(global).with(ctx).with(sym, kind)
```
But `f(global).with(sym, kind)` would give a type error.

With clauses can be freely interspersed with normal parameters, but a normal parameter clause cannot
directly follow a with parameter clause consisting only of types outside parentheses. So the following is illegal:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with parameter

@sideeffffect
Copy link
Contributor

sideeffffect commented Jan 21, 2020

I also don't understand why we're keeping "context bounds" around -- they're a relict of a time when declaring implicit/given/with parameters was awkward. But those times will soon be gone and we will be able have things like this just fine:

def aggregate[A] with Monoid[A] (list: List[A]): A =
    ???

And there should be just one way of declaring implicit parameters IMHO.

Btw, declaring implicit parameters first, and the ordinary after that has the benefit that it makes passing methods with implicit parameters easier:

def f(g: List[Int] => Int): X = ???
given as Monoid[Int] = ???
f(aggregate)

before one had to do this

def aggregate[A](list: List[A])(implicit A: Monoid[A]): A = ???
def f(g: List[Int] => Int): X = ???
implicit val xxx: Monoid[Int] = ???
f(aggregate(_))

@odersky
Copy link
Contributor Author

odersky commented Jan 21, 2020

@sideeffffect I believe context bounds are still a lot more concise and easier to read than the alternatives.

The latest commits allow normal parameters after given parameters, but it's fair to say that this point is still contentious.

@ri-cisse
Copy link

When defining typeclasses, why do we need to a modifier on the params? We can make given keyword required only when passing context params.

// Defining typeclasses

given [T] (o: Ord[T]) => Ord[List[T]] {}

given [T] Ord[T] => Ord[List[T]] {}
given [T: Ord] => Ord[List[T]] {}

given Ord[Int] {}

given [T, U] (t: Ord[T], u: Ord[U]) => Ord[List(T, U)] {}
given [T, U] Ord[T], Ord[U] => Ord[(T, U)] {}

given [T, U] (t: Ord[T]) => (u: Ord[U]) => Ord[List(T, U)] {}
given [T, U] Ord[T] => Ord[U] => Ord[(T, U)] {}

given ExecutionContext = ExecutionContext.global

// with names

given listOrd[T] (o: Ord[T]) => Ord[List[T]] {}

given listOrd[T] Ord[T] => Ord[List[T]] {}
given listOrd[T: Ord] => Ord[List[T]] {}

given intOrd => Ord[Int] {}

given listOrd[T, U] (t: Ord[T], u: Ord[U]) => Ord[List(T, U)] {}
given listOrd[T, U] Ord[T], Ord[U] => Ord[List(T, U)] {}

given listOrd[T, U] (t: Ord[T]) => (u: Ord[U]) => Ord[List(T, U)] {}
given listOrd[T, U] Ord[T] => Ord[U] => Ord[List(T, U)] {}

given ec => ExecutionContext = ExecutionContext.global


// summoning instances

def max[T](lst: List[T])(given o: Ord[T]): Option[T] = ???
def[T] (lst: List[T]) max(given o: Ord[T]): Option[T] = ???
def[T: Ord] (lst: List[T]) max: Option[T] = ???

extension [T] on (s: List[T])(given Ord[T]) {
  def max: Option[T] = ???
}

extension [T: Ord] on (s: List[T]) {
  def max: Option[T] = ???
}

@main main = {

  max(List(1,2))
  max(List(1,2))(given summon[Ord[Int]])
}

@odersky
Copy link
Contributor Author

odersky commented Jan 21, 2020

@ri-cisse

When defining typeclasses, why do we need to a modifier on the params? We can make given keyword required only when passing context params.

That is in fact what's currently implemented. People objected on the grounds that the conditional clauses in given instances looked like function types. I am not so sure about this, but anyway, using with also looks fine. There's no longer a pressing reason to invent different syntax for conditional given instances.

@odersky
Copy link
Contributor Author

odersky commented Jan 22, 2020

If there is no further input I'd like to merge this by tomorrow. Not sure it's feasible to review this (it's quite a large delta and not very interesting technically) but if someone wants to review parts of it, please go ahead.

@jdegoes
Copy link

jdegoes commented Jan 22, 2020

👍 for merging.

@smarter
Copy link
Member

smarter commented Jan 22, 2020

Now that we have a very lean syntax for implicit parameters, can we drop https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/transform/ShortcutImplicits.scala ? It optimizes methods whose result type is an implicit function type but those can now be written almost as succinctly using with ..., and the implementation of ShortcutImplicits is very complex and probably still buggy (I remember someone hitting a crash in it on gitter recently).

@odersky
Copy link
Contributor Author

odersky commented Jan 22, 2020

@smarter lean syntax helps, but if you have many context parameters it's still needless duplication. There's value in abstraction if the abstraction is zero cost. Right now ShortcutImplicits duplicates code, so it is not really zero cost. I was thinking that maybe we can use a slightly different approach, where we always use the "shortcut" representation instead of the curried one and rely on bridge construction to keep the right overriding relationships. That would mean rolling ShortcutImplicits into erasure.

Was updated to new syntax by accident
Changes should be rolled out only after released.
For given instances:

    given ...

For context parameters

    ... with ...

For context functions

    A ?=> B
It's now
```scala
given _
given T
```
Thta way it's more regular: A given selector is always followed by something: Either
a wildcard or a type.

The previous syntax for type bounds on normal imports is no longer supported.
We might want to bring it back at some point, but it's not essential.
There are valid use cases, and syntactic awkwardness can be kept in check
by the "one space following a with clause" rule.
Address comments by @soronpo
@odersky odersky merged commit 9136d78 into scala:master Jan 23, 2020
@odersky odersky deleted the change-given-with branch January 23, 2020 14:05
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

Successfully merging this pull request may close these issues.

9 participants