Updated Proposal: Revisiting Implicits

Oh lol, yeah I fixed that, it was a copy-paste mistake.

Is the distinction still not clear after the fix?

The syntax in the docs explains that aspect: as must be used if any of the three optional components that precede it is present. These are

  • a name
  • a type parameter list
  • using clauses

Yes, I know you can given t as T = x. That part’s fine.

You are correct that I am explicitly complaining about the mental shift needed to go between named given definitions and anonymous given aliases.

The reason is that these two are the most common ones I want to use. The given t as T = x form is nice to have for future-proofing, but is kind of pointless; we already have a name for t: x! And if you’re porting old code, you already have the name for given t as T { ... } because it used to be implicit val t: T = new T { ... }.

Like I said, I don’t have a great solution. It just is jumpy in a way that I don’t think will entirely go away with time, at least not with everyone. Coupled with the awkward grammatical form, I think it’s liable to make the feature less appreciated than it should be.

Just wanna say that I love this new using keyword is introduced! :slight_smile:
We use given for providing an implicit, and using for taking an implicit parameter.
Those are two different things, and it’s much, much more readable this way! Thanks!

1 Like

Thanks for the positive feedback!

2 Likes

I believe we are on the home stretch. given/using is the clear winner as far as I can see.

There’s one detail that we should still discuss. We currently use given/as for instances everywhere. I.e.:

given intOrd as Ordering[Int] ...
given t as Table
given ctx as Context = ForkJoinContext()

One reason to do so was that the form

given t: Table

looks like an abstract definition, which it is not. In fact,

given t as Table

is analogous to

implicit object t extends Table

A body is not required in either case. This analogy of as with extends is a good reason for keeping it for regular instance definitions.

But for aliases we do have a choice. Maybe it should be

given ctx: Context = ForkJoinContext()

instead? This would introduce an irregularity wrt regular instance definitions, but would align given aliases with using clauses and also with given in patterns. Indeed I can already write

val (given ctx: Context, n) = (ForkJoinContext(), 42)

Here, the given prefix marks the pattern-bound variable ctx as a given.

So far, the compiler supports both : and as for given aliases, but we will have to drop one of them. So, which one should it be?

[EDIT] Another reason for : in aliases is the special case of expressing a given whitebox inline function. I.e., the equivalent of

implicit inline def f(using A) <: B = ${...}

With : this would be

inline given f(using A) <: B = ${...}

But with as, we have instead

inline given f(using A) as _ <: B = ${...}

The latter needs another piece of specialized syntax.

3 Likes

I like it

Another argument for using : instead of as in given aliases, is that in SQL as is used for aliases but in a different meaning. So there could be some “false friends” confusion for someone coming from SQL learning Scala seeing “given alias” and as.

On the other hand:

The syntax given [T]: Foo[T] = foo looks different than any other usages of : in Scala (as far as I know). Would it be possible to write simply given [T] Foo[T] = foo instead?

Would it be possible to write simply given [T] Foo[T] = foo instead?

That’s also different from all other usages of type parameters. So I doubt it’s an improvement. In the revised proposal it would be:

given [T] as Foo[T]
given [T] : Foo[T] = ...

Is

given [T] : Foo[T] = ...

so much stranger than

given [T] as Foo[T]

?

In most Scala 2 code, I generally read “: Foo” as meaning “has type Foo”, or “returns type Foo”. I know that it is also used for Context Bounds (but don’t use those), I don’t know how else it is used in Scala 2.

I don’t know if that aligns with how you propose “:” is used in Scala 3? E.g. does it retain the same logical meaning for givens? I think that you have also proposed using it for the quiet syntax, which I’m not keen on, because it seemingly gives “:” another different meaning.

But, I have to say, as a user of the language I would still prefer easier syntax for the common stuff. E.g. I see that context parameters as one of the easier usages of givens that seems to solve a practical problem in a nice way. I think that I would rather see a separate keyword, e.g. like ‘context’ rather than the generic ‘given’, when used for this scenario.

E.g.

context ctx: Context = ForkJoinContext()

Also, it seems strange to me to have “val” in the next example, but not in the example above when a pattern isn’t being used:

val (given ctx: Context, n) = (ForkJoinContext(), 42)

I also don’t quite follow that for some features it seems that you are very happy to have extra bespoke syntax to make the language easier (or just different from before), but for other very complex features (e.g. givens) you are avoiding that. I don’t know if this is your intention, but as observer t feels like the language is being optimized for experts only - which I don’t see as a path to increase adoption of the language - quite the reverse in fact.

I think that the Scala 2.8 collections library suffered from second system syndrome, in that it had too much complexity leaking out of the abstraction (e.g. type signatures and breakout), a lot of which seemed to be stripped out and cleaned up for the Scala 2.13 collections (that are much better in this regard). I have to say that I wonder whether Scala 3 is going down the same path. I.e. some excellent ideas, but with a significant increase in complexity to go with it.

4 Likes

In isolation it might not be stranger. But to me, it looks strange when compared to other usages of : e.g. name: Type or the general form “x: y”. I read it as given ([T] : Foo[T]).

from the documentation on polymorphic function types:

val id = [T] => (t: T) => t

Why not then also

given [T] => Foo[T]

again this builds on my preference for the previous => syntax, we could

given [T] => Ord[T] => Ord[List[T]]

Why not?

3 Likes

given t as Table reads like a cast to me. as is used in the collections APIs to convert from one kind of container to another.

given t for Table seems much closer to what is actually happening. Or if we weren’t trying to reuse for the most direct text would be: given Table is t.

4 Likes

Yes, colon / equals works much better, I think!

Any chance we can allow object Foo as Bar { ... } too (maybe not right now, but sometime in the future)? And val x as Abstract{ def method = "implementation" }?

Because right now it’s super awkward at times (requiring val x: Abstract = new Abstract { ... }), and this would unify everything in a compact and regular way.

(Edit 1: Even without this unification–which I admit is not a perfect parallel given the function form of givens that doesn’t have a parallel–I think colon+equals is the way to go.)

(Edit 2: this unification suggests is is a better word than as, even though as feels superficially more like what you’d say with given…but I think that feeling comes from a mis-parsing of the linguistic usage of given here.)

maybe because t as Table is a kind of a cast in C#

After having tried out this change on all our docs and most of our code I have come to the conclusion that it’s probably better to leave it as it is, i.e. given/as everywhere.

given/: for simple aliases does feel more natural, but there are also downsides:

  • The most important part of a given is the implemented type. That’s different from other definitions, where the name of the definition is usually the best clue what’s defined. In a complex given alias, the : is just not visible enough to make out the type easily. Here’s a real world example:

    given t3[T, U, V](using st: Show[T], su: Show[U], sv: Show[V]): Show[(T, U, V)] {}
    

    It’s very hard to make out what’s defined here. Compare with

    given t3[T, U, V](using st: Show[T], su: Show[U], sv: Show[V]) as Show[(T, U, V)] {}
    

    and factor in that the as will be keyword highlighted. It does make it much easier to identify the defined type. This is a fairly typical complex use case, there are far worse cases than this around.

    A variant of this problem is having to decide at a glance whether the parts immediately following the given are the type that’s implemented or the name and parameters. I.e.

    given SomeThing[A, B <: A](using SomeThingElse[A, B]): Result = ...
    

    vs

    given SomeThing[A, B <: A](using SomeThingElse[A, B]) = ...
    

    The two meanings are quite different, yet hard to distinguish visually. Again, as makes the distinction easier.

  • Diminished regularity. The syntax of given instances takes some time to teach since it is so multi-faceted. Adding another distinction (i.e. between sometimes as and sometimes :) makes that harder. Right now here is how I would teach given instances:

    • A given instance starts with given, followed, optionally, by the name of the given instance, its type parameters, any context parameters and as. Then comes the implemented class. After that you can continue with either = (if the given is an alias of some existing value) or a list of member definitions of the implemented class.

    Having to distinguish between as and : would make that more complex and harder to remember.

I did not think I would come to this conclusion before I started changing code to the alternative given/: syntax. But I found myself squinting at a lot of definitions that were made harder to read than before. So in the end I decided to abandon that experiment, and am now in favor of keeping things the way they are defined in the current release.

[UPDATE] After porting the rest of our codebase including the community build to uniform given/as I am now even more convinced that its the right choice. For simple, monomorphic aliases, it’s a toss up: more convenient syntax with : vs more regularity with as. But about half of the (estimated 400-500) occurrences of given aliases were not of the simple kind, and there an as improved legibility each and every time.

Another strong reason to prefer as is this one: If you expect to see at some point a term of the form “a: b” then the “b” part is usually optional, so it could be just an “a”. But if you expect an “a as b” then the “a” part is optional, so it could be just a “b”. So : and as have opposite conventions about which side is optional. Using : for some usages of as would sow confusion about this rather fundamental point.

1 Like

Oh, yes, that is pretty unreadable. I agree; we’d better stick with as. If aliases were limited to simple values, it would be different. But aliases to methods do seem inordinately hard to parse.

Too bad! It had seemed quite promising.

given Context {
  ...
}

At first glance I would like that ‘as’ were mandatory.

given  as Context {
  ...
}

It is easy for cursory reading.
But it is not very convenient if the most “given” are anonymous.

So may be it will be better if naming-conventions require that names must be in lower camel case

Yes, I think that’s a useful convention that can help here.