Skip to content
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

Trial: `given as` instead of `delegate for` #6773

Merged
merged 2 commits into from Jul 4, 2019

Conversation

@odersky
Copy link
Contributor

commented Jul 1, 2019

Since we are about to try out different syntaxes, here's another trial: Use given as instead of delegate for.

Why?

Reactions after the introduction of delegate were mixed. Most people acknowledged that it was better than implied, but stated that they were not completely comfortable with the new syntax either. It's true that so far every alternative proposal was mostly disliked (until it was withdrawn, that is, that's when votes in favor tended to come out). At the same time there were suggestions to use given as a noun (from Miles, Adriaan, maybe others) that felt to me like it could work. So I do not intend to re-open a general brainstorming for good terms. We got nowhere the last time we tried that. But it's worthwhile to try this single idea and compare it to delegate (which as far as I am concerned would work as well).

What's in the PR?

An alternative syntax

given IntOrd as Ord[Int] { ... }

in place of

delegate IntOrd for Ord[Int]

Both syntaxes are allowed (the docs in the PR changed to describe the new one). The idea is to keep just one alternative after about one release cycle and drop the other. During that release cycle it would be good to give the syntax maximal trial exposure.

Alternatives

There is one potential issue with the new syntax, in that given is now used as the name of implicit instances as well as implicit parameters. In that sense, it's similar to what implicit was before. This might look confusing, as in this example:

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

We might want to think about different keywords for the parameter. Miles proposed where, but that
does not work so well if what we pass is a context instead of a typeclass instance. E.g. in dotc itself,

def isRef(sym: Symbol) where Context = ...

reads not as nicely as

def isRef(sym: Symbol) given Context = ...

Another possibility is to go back to with, which is generic enough to mean anything, but is also annoyingly unspecific.

Or we might conclude that there is no problem with given, after all, which is my current default assumption. In any case conditional givens of the form above are not that common, since a lot of them
would be written with context bounds instead.

given ListOrd[T: Ord] as Ord[List[T]] { ... }
@propensive

This comment has been minimized.

Copy link
Contributor

commented Jul 1, 2019

I'm tentatively enthusiastic.

@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 1, 2019

Direct link to docs:

https://github.com/dotty-staging/dotty/tree/change-given-syntax/docs/docs/reference/contextual

There is also a change to import delegate. First it's import given now, of course, but second the import by type syntax has changed. It's now _: Type instead of for TypeList. The type bound syntax can be used for any wildcard import or export, not just imports or exports of given instances. The docs in

https://github.com/dotty-staging/dotty/blob/change-given-syntax/docs/docs/reference/contextual/import-delegate.md

show a use case for a regular export.

@odersky odersky force-pushed the dotty-staging:change-given-syntax branch from b921b7b to fdb718f Jul 1, 2019

@jducoeur

This comment has been minimized.

Copy link
Contributor

commented Jul 1, 2019

I agree with Jon: still mulling it over, but I like given more than delegate -- I think it reads a bit better, and provides some natural parallelism in usage that may make it slightly easier to learn.

@sjrd

This comment has been minimized.

Copy link
Member

commented Jul 1, 2019

I'll voice my support for given as, based on two things:

  • given for parameters seems to have been well received
  • using given as restores a symmetry between "initial" givens and parameter-provided givens (when searching for candidates to use as actual given parameters, we now look for givens in scope, instead of givens+delegates in scope)
@jdegoes

This comment has been minimized.

Copy link

commented Jul 1, 2019

In order of preference:

  1. instance + given (already vetoed for various reasons)
  2. default + given (suggested several times, don't know if vetoed)
  3. canonical + given (suggested once?, don't know if vetoed)
  4. delegate + given (reasonable choice)
  5. standard + given
  6. given + given (ok but awkward to talk about "givens")
  7. ...
  8. Anything related to implicit (due to overloading and past connotations, fresh break is important IMO)

Although honestly I will be happy with any solution which is not implicit because the name matters primarily as a first-impression, and assuming no pre-existing connotations, it will be possible to teach a fresh name.

@sideeffffect

This comment has been minimized.

Copy link
Contributor

commented Jul 1, 2019

my 2 cents:
I like, if the keywords for declaration and use of a typeclass instance are

  • related (by the meaning how these words are understood in English), because it makes it easy to understand/explain by/to newcomers
  • but not the same, because declaration and use are also two distinct concepts -- also helps with understanding/explaining; (given as + given don't sound distinct enough to me personally)

these 2 guidelines are IMHO the most important thing, the particular keywords are not that important
but here are my suggestion anyway:

  • give + given
  • offer + given
  • offer + offered
  • ...

👍 on not using implicit, because googling that would return results for "old way" of doing typeclasses

@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 1, 2019

default + given (suggested several times, don't know if vetoed)

This was considered but discarded since default is too commonly used to make it a keyword now.

@ctongfei

This comment has been minimized.

Copy link

commented Jul 1, 2019

I really like given as the keyword to introduce inferred parameters -- it reads really nice.

A new proposal:
The current delegate proposal's underlying implementation is a class (I presumed so, please correct me if I'm wrong). Normally (e.g. in cats), we declare a type as

class ListOrder[A](implicit A: Order[A]) extends Order[List[A]] { }

I propose to change the extends keyword to proves or gives, that yields the following syntax:

class ListOrder[A] proves Order[List[A]] given Order[A] { }

or

class ListOrder[A] gives Order[List[A]] given Order[A] { }

This is syntactically familiar with OO programmers. If proven is too PL-theoretic, we could use gives: I like the symmetry of gives/given.

@jeremyrsmith

This comment has been minimized.

Copy link

commented Jul 1, 2019

As an early skeptic of changing implicit to begin with, I really like given ... as ... and given parameters. It aligns well, consumes only one keyword, and unifies into one concept where the definition mirrors the usage.

I think that given Ord[Int] as IntOrd (i.e. the type first) reads nicer but I don't care much (and it would probably complicate things). 👍 to this change either way.

@jeremyrsmith

This comment has been minimized.

Copy link

commented Jul 1, 2019

Another bonus to given Ord[Int] as IntOrd { ... } is that if you define an anonymous instance it reads quite nicely:

given Ord[Int] as {
  ...
}
@milessabin

This comment has been minimized.

Copy link
Contributor

commented Jul 1, 2019

I'm happy with using given for both the introduction and elimination sides. I have a mild preference for retaining the current use of for rather than switching to as, but I could live with either.

@dwijnand

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

I agree given is an improvement over delegate (as well as implied). 👍

But I think I'd prefer if anonymous instances didn't have the trailing as, so instead of:

given as Ord[Int] { ... }

have

given Ord[Int] { ... }

which I read as "given an Ord of Ints, defined as ....".

Secondly, I'm particularly unsure about as because:

  1. We have a convention in and around the standard library to use as to define conversions that wrap instead of copy (recent example scala/scala-java8-compat#116) and here neither wrapping nor copying is happening, it's just syntax.
  2. as is probably even more commonly used, at least in example/doc code, than default, no?
val as: List[A] = ...
DelegateBody ::= [‘for’ ConstrApp {‘,’ ConstrApp }] {GivenParamClause} [TemplateBody]
| ‘given’ GivenDef
GivenDef ::= [id] [DefTypeParamClause] GivenBody
GivenBody ::= [‘for’ ConstrApp {‘,’ ConstrApp }] {GivenParamClause} [TemplateBody]
| ‘for’ Type {GivenParamClause} ‘=’ Expr

This comment has been minimized.

Copy link
@dwijnand

dwijnand Jul 2, 2019

Contributor

Still uses for

@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 2, 2019

I am open to use as or for but think it has to be one of them, or the grammar would become too ambiguous. Right now the as or for is the only anchor point that allows the parser to correctly classify

given A as B
given C[X] as D

as two given definitions and

given A as B
given C[X]

as a single definition with a given clause.

as would be a soft keyword, significant only in this one place. In that sense it would be similar to derives, which is also a soft keyword.

Not sure which one works better. I felt given as flowed more naturally than given for but if the majority of English speakers disagrees I am OK if we go the other way.

@dwijnand

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

Sadly neither given as Ord[Int] nor given for Ord[Int] make any English sense.

@jeremyrsmith

This comment has been minimized.

Copy link

commented Jul 2, 2019

I think as makes more sense with the type name first:

given Ord[Int] as IntOrd { ... }
given Ord[Int] as { ... }

and for makes more sense with the instance name first:

given IntOrd for Ord[Int] { ... }
given for Ord[Int] { ... }

though the former reads more nicely when defining an anonymous instance.

@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 2, 2019

Sadly neither given as Ord[Int] nor given for Ord[Int] make any English sense.

You have to twist it around then. The thing that's actually given follows.

@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 2, 2019

@jeremyrsmith The way I see it is that the first is the actor (the implementation), the second the role (the type). So it's "Given Daniel Craig as James Bond, ... ", not "Given James Bond as Daniel Craig".

@dwijnand

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

Any chance we can use :?

given IntOrd: Ord[Int] as { ... }
given Ord[Int] as { ... }

and for that disambiguation case you gave:

Right now the as or for is the only anchor point that allows the parser to correctly classify

given A as B
given C[X] as D

as two given definitions and

given A as B
given C[X]

as a single definition with a given clause.

It would become

given B: A as { ... }
given D: C[X] as { ... }
// two given definitions

given B: A
given C[X] as { ... }
// one definition with a given clause

WDYT?

@jeremyrsmith

This comment has been minimized.

Copy link

commented Jul 2, 2019

So it's "Given Daniel Craig as James Bond, ... ", not "Given James Bond as Daniel Craig"

Not sure about that analogy 😀 since Daniel Craig (presumably) plays many roles, but an instance will only play one role – so the role is the thing that's important (and indeed I think most people will not even bother naming instances most of the time now that anonymous instances are allowed).

given Ord[Int] as { ... } says, "There is an ordering for Int and here is its implementation". I think given as Ord[Int] { ... } says "Here's a thing that's given and it happens to be an Ord[Int]." It's a small but meaningful (IMHO) difference.

But anyway, I don't think it matters that much. given is a step forward for readability so I'l leave it be and be happy with it. Sorry for the persistent bikeshedding 😄

@LPTK

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

@dwijnand
To me, : sounds like the more logical and understandable syntax to use here, because everyone knows that in Scala this symbol is used to say that something is an instance of a type, and making type class instances is fundamentally about that.

At the risk of appearing heretical, I'd propose to use : but also go all the way to making the syntax more regular with the rest of the language by using the "naked new" syntax that's already available in Dotty:

given IntOrd: Ord[Int] = new { ... }

// Anonymous:
given: Ord[Int] = new { ... }

// Conditional:
given OptionOrd[A] given Ord[A]: Ord[Option[A]] = new { ... }

This also unifies the syntax with given aliases, simplifying the language.

given global: ExecutionContext = myForkJoinPool

I can see the criticism coming, but don't think this is really "exposing the mechanism rather than the intent." Behind the scenes, the instance may still be cached or not, like with the current proposal.

On the other hand, I think the fact we are creating a new instance of a type is really better made apparent, using the syntax that's familiar for these concepts. I'd wager that no beginner will ever be comfortable and productive using type classes before they understand these basic things, so it's best not to hide it from them.

@sjrd

This comment has been minimized.

Copy link
Member

commented Jul 2, 2019

The syntax with new is problematic because it already means something that is different. In particular, public members defined in the given but not in its super-class would not be visible outside using new, but are visible outside now (like definitions in objects).

@LPTK

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

@sjrd I don't think the public members would be visible from the outside, because the type of the instance is annotated (and the type annotation would be required by the compiler). In this respect,

trait T
given A: T = new { val x = 0 }
A.x // error

would behave the same in that respect as:

trait T
def A: T = new { val x = 0 }
A.x // error
@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 2, 2019

We want to get away from new, so let's not find new (excuse the pun) use cases for it. 😸

@jdegoes

This comment has been minimized.

Copy link

commented Jul 2, 2019

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

On further reflection, what bothers me a little here is that the arrows are flipped: the first given means an instance is provided to the environment; while the second given means an instance is required by the environment.

In teaching Scala 2.x, I have had to explain that the implicit in implicit val foo means that the value foo will be made available to (provided to) implicit search resolution; and that the implicit in def foo(implicit bar: Bar) means that the value bar will be required from (given from) implicit search resolution. The arrows are going opposite directions.

They are two different meanings hiding behind the same keyword.

provide is not commonly used as a noun, but the difference is something like:

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

As an aside, I do like : , mainly because it's one fewer construct and can be explained in terms of the existing type ascription operator:

provision ListOrd[T]: Ord[List[T]] given Ord[T] { ... }

provision _ : Ord[List[T]] given Ord[T] { ... }

// ???
// provision: Ord[List[T]] given Ord[T] { ... }

for some suitably better provision (maybe just given, despite the overloaded meaning).

@sjrd

This comment has been minimized.

Copy link
Member

commented Jul 2, 2019

@jdegoes given parameters already mean both

  • require the thing to be available at call site, and
  • provide the thing at definition site.

As such, the left-most given provides the thing at definition site, which because it's top-level, means everywhere where it's imported/in scope.

It makes sense to me that way.

@jdegoes

This comment has been minimized.

Copy link

commented Jul 2, 2019

given parameters already mean both

In teaching, I would not present that model, as it's both unnecessary and confusing.

I would teach only that given means construction or invocation has contextual requirements.

As such, the left-most given provides the thing at definition site, which because it's top-level, means everywhere where it's imported/in scope.

This is the thing that bugs me: the "left-most given" means "provide".

I don't really care all that much, but generally I find teaching is easier when concepts with separate semantics have separate names.

@SethTisue

This comment has been minimized.

Copy link
Member

commented Jul 3, 2019

as I've said elsewhere, I'm glad we are still exploring alternatives to delegate.

the repetition of given seems awkward to me, for reasons that have already been covered. I like the suggestion (made most recently by @sideeffffect) to use a verb such as give, offer, or provide instead.

I find for more natural than as; I don't see the advantage in changing that one. (and, as @dwijnand said, the existing association with as in Scala with wrappers is pretty strong. here, we already have the right type, we aren't turning something into a different type.)

@yinshurman

This comment has been minimized.

Copy link

commented Jul 3, 2019

I suggested to use the word pick to replace delegate.I am glad that it's still possible to have alternatives for delegate.It seems that most people don't like delegate. personally I prefer the pick for syntax than given as . I am also agree with that for is better than as, to use for we only need to add one new key word such as pick ,delegate ,give or provide . On the other hand ,as may be frequently used in real world production softwares already. so it is better not to make it a key word.

@sideeffffect

This comment has been minimized.

Copy link
Contributor

commented Jul 3, 2019

declaration of a type-class instance is in some ways similar to a declaration of an object.

with object declaration, the most important thing is the name of the object, and sometimes it can extend a trait or something:

object X {
  ...
}
object Y extends Z {
  ...
}

with type-class instances, the type is the most important thing, but we can optionally give a name to the instance:

give Ord[Int] {
  ...
}
give IntOrd extends Ord[Int] {
  ...
}

(or offer IntOrd extends Ord[Int] { or provide IntOrd extends Ord[Int] {)
object and give/offer/provide are like two sides of the same coin. And it's made obvious by using the same syntax (A extends T), that's why I like extends
btw that's why I don't see need for any as, for, =, just like we don't use them in object Abcd { ... (but I'm not intimately familiar with the grammar though)

Edit note: I reconsidered my thoughts on the use of :

@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 3, 2019

I think I still like as better, because I see it as a shorthand for

given X as (the canonical instance of) T
@Jasper-M

This comment has been minimized.

Copy link
Contributor

commented Jul 4, 2019

@jdegoes

In teaching, I would not present that model, as it's both unnecessary and confusing.

Then how do you explain that a given parameter is also "provided" in the local scope? Whether or not it's confusing might be open for interpretation, but it's not unnecessary.

I have to agree though that the repetition in given ... as ... given ... looks a bit weird.

@jdegoes

This comment has been minimized.

Copy link

commented Jul 4, 2019

Then how do you explain that a given parameter is also "provided" in the local scope? Whether or not it's confusing might be open for interpretation, but it's not unnecessary.

You don't need to explain it that way. You say:

If your function sort requires the ability to compare values of type A, then you state this requirement in the type signature of the function:

def sort[A](list: List[A]) given Compare[A]: List[A]`

Now sort has the ability to compare values of type A inside the body of the function, because this requirement is stated in the type signature. Anywhere inside the body, you can use comparison operators like >, which are defined inside Compare.

You may only call sort with values of some type if the type provides your function what it needs: which is the ability to compare values of that type.

For example, you can only call sort with List[Int] if the type Int provides your function the ability to compare values of type Int.


Good teaching benefits from many techniques, but the 2 most relevant here are:

  • Elision of mechanical details in favor of a simpler semantic model. In this case, the simple model is: state what your function requires of the types it operates on.
  • Multi-stage explanations that progressively refine earlier stages. In this case, the refinement is that you may not call your function (which requires something from a type) unless that type provides what the function requires.

odersky added some commits Jul 4, 2019

Allow newlines before parameter lists
Allow newline characters before a `(` token
that starts a formal parameter list.

@odersky odersky force-pushed the dotty-staging:change-given-syntax branch from bb073b4 to e9e926c Jul 4, 2019

@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 4, 2019

I squashed all relevant commits into one so that we can easily revert. Anybody wants to review the technical details or should we merge as is?

@milessabin

This comment has been minimized.

Copy link
Contributor

commented Jul 4, 2019

I've just given it a once-over ... LGTM.

On a line-by-line basis at least, given/as reads a lot better than delegate/for both in the code and the docs.

:shipit:

@odersky

This comment has been minimized.

Copy link
Contributor Author

commented Jul 4, 2019

OK, merging. To be sure, it's trial only at this stage.

@odersky odersky merged commit 3c99306 into lampepfl:master Jul 4, 2019

2 checks passed

CLA User signed CLA
Details
continuous-integration/drone/pr the build was successful
Details

@liufengyun liufengyun deleted the dotty-staging:change-given-syntax branch Jul 4, 2019

@nafg

This comment has been minimized.

Copy link

commented Jul 5, 2019

@Jasper-M

This comment has been minimized.

Copy link
Contributor

commented Jul 5, 2019

If we aren't keeping the problems clearly in mind then our solutions may not fit.

I don't think there's full consensus about which things are actual problems and which are not.

@nafg

This comment has been minimized.

Copy link

commented Jul 5, 2019

@nafg

This comment has been minimized.

Copy link

commented Jul 5, 2019

@milessabin

This comment has been minimized.

Copy link
Contributor

commented Jul 5, 2019

@odersky shapeless-3 migration from implicits to givens here: milessabin/shapeless@cd0ac77. By and large I would say this is a change for the better.

@spf3000

This comment has been minimized.

Copy link

commented Jul 6, 2019

I don't think it's a winner. given would then be used in two different ways with two different underlying semantic meanings. given as I believe refers to a known or established fact "at a couture house, attentive service is a given" or "IntOrd is a given" (noun). The other is more like taking into account e.g. "given the complexity of the task, they did a good job" or "given an Ord[T] I will provide a List[Ord[T]]. The second one is very neat the first quite clunky, and the difference between the two hard to explain.

@hseeberger

This comment has been minimized.

Copy link

commented Jul 8, 2019

I agree with @jdegoes, having "two different meanings hiding behind the same keyword" is confusing, in particular for learners. I like "given" for requiring, but not for defining. "delegate" was fine for me (simple change: just revert the commit), other suggestions like "instance" or "impl" are also fine – let's just use a different word.

@ashwinbhaskar

This comment has been minimized.

Copy link
Contributor

commented Jul 11, 2019

IMHO, delegate was a better option. Can I know what are the objections to delegate? Other than not comfortable with the syntax.

@megri

This comment has been minimized.

Copy link

commented Jul 22, 2019

Has term … for … been considered? Since the mechanism is now called term inference, it seems like it would make sense.

@hansdejong

This comment has been minimized.

Copy link

commented Jul 27, 2019

FWIW (I’m hardly using implicits):
My preference is “autocast” in stead of "given".
Then the difference between “as” and “for” is clear:
“autocast A as B” goes from left to right
“autocast B for A” goes from right to left

The sort example becomes something like:
def sort[A](list: List[A]) autocast Compare[A] for List[A]

@Ichoran

This comment has been minimized.

Copy link

commented Jul 29, 2019

I don't think given given is any better than implicit implicit. Using the same word--not clearly related words, but actually the same series of characters--for two different contexts is awkward because it forces us to know the context in order to understand the word, instead of being able to use the word to understand the context.

Especially when lines get long this seems really unfortunate.

given MyReallyLongType[T]
  as MyReallyLongMoreExtensivelyParameterizedType[T, SomeType]
  given ThisOtherNeededType[T] ...

At least with implicit and implicit, the requirement side was in parens so it was hard to confuse.

I don't have a positive suggestion for the declaration site beyond agreeing that delegate isn't great. I think given is worse, though--not just worse for teaching, but worse for code comprehension since it requires the reader to keep more mental state alive in their head.

(For similar reasons, not that it's being proposed, I also oppose adding val to usages of an identifier: val x = 5; val y = val x * 7.)

@rjolly

This comment has been minimized.

Copy link
Contributor

commented Aug 4, 2019

@jdegoes wrote:

In order of preference:

1. `instance` + `given` (already vetoed for various reasons)

Could someone point me to where it was discussed ? instance is what is used in Haskell, so at least it would acknowledge the anteriority.

@neko-kai

This comment has been minimized.

Copy link

commented Aug 5, 2019

@rjolly It was discussed briefly in the Scala Contributors forum; was rejected since instance in OOP has a specific meaning already and it clashes with object we already have.

@rjolly

This comment has been minimized.

Copy link
Contributor

commented Aug 5, 2019

@kaishh Thanks. witness was not so bad, despite @lihaoyi 's argument that googling for it does not yield the expected result, as stated in #5458 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.