Closed odersky closed 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
:
given
is a construct by itself, not a modifier for other definitions, so its syntax focuses on intent over mechanism,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
.
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
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.
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
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.
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:
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 symmetricalas
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:
My proposal:
given IntOrd as Ord[Int] { ... }
Use a column:
given IntOrd: Ord[Int] { ... }
given as Ord[Int] { ... }
Use nothing, like in a method parameter list, or an implicit function type:
given Ord[Int] { ... }
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]] { ... }
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)
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
}
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.
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.
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!
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
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.
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.
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.
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) { ... }
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.
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)
(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)
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.
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.
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)
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.
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.
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?
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.
It seems we have implicit consensus on
given
as the new name for implicit instances. It has a lot more support than its alternativedelegate
. So, let's take that as a given šgiven
is a good name for instances, but it does have a potential problem thatgiven
parameters andgiven
instances are too easily confused. The example that made this painfully clear for me was in #7056, where we find: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 putgiven
under parentheses, thereby using the standard way of expressing parameter dependencies. I.e. it would beinstead of
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
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
instead of
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.