Pre-SIP: Additional syntactic sugar for typeclasses

Sorry I confused * with _. But this should do the trick with -Ykind-projector in Scala 3. I believe at some point this is intended to change to _ iirc.

Thanks, it does indeed work with * and the flag, and yes you’re right that in 3.2 it will work with _ and no flag (IIUC). ? : Showable has my vote then.

I’ve read this proposal a couple times over. I am sympathetic with the motivation for having some unnamed context bound for an unnamed type parameter. I never liked that we introduced unnamed given and using instances, but since we did, it is not a stretch to also want them in this context.


Now this is one of those proposals where the desired semantics are pretty much clear and unarguable. But not completely. Using the hypothetical :-based syntax, it’s pretty clear that

def showAll(xs: List[? : Showable]): Unit

must desugar to

def showAll[T : Showable](xs: List[T]): Unit

which itself already has an existing desugaring as

def showAll[T](xs: List[T])(using Showable[T])): Unit

However, it’s not so clear when the original definition defines a type parameter of its own. For example

def showAll[A](xs: List[? : Showable], y: A): Unit

(don’t be hung up on what the purpose of that method would be; it’s irrelevant)
Now suddenly we need to add a type parameter, but there is already one (or several). There is no precedent in Scala for inserting a type parameter this way.

The closest that comes to it is generic extension clauses with generic methods. For example

extension [A] (xs: List[A])
  def myFoldLeft[B](z: B)(f: (B, A) => B): B = ...

which desugars into a method with multiple type parameter lists:

def myFoldLeft[A](xs: List[A])[B](z: B)(f: (B, A) => B): B = ...

The difference is that here, there is always a term parameter list in the middle.

Based on that precedent, I believe that we should add a synthetic parameter list. To preserve natural call sites, I think the additional type param list should come after the first one. So

def showAll[A](xs: List[? : Showable], y: A): Unit

would desugar as

def showAll[A][T : Showable](xs: List[T], y: A): Unit

Now, about the syntax.

First, it’s been mentioned a few times to "just do like Rust and use impl Showable". I am very opposed to that for two reasons:

  1. While hard-core FP developers don’t like to hear it, in Scala, implementing something is an inheritance concept: implementing a trait with extends, implementing an abstract def in a subclass/subtrait, etc. So if we see List[impl Showable], it reads as a list of something that implements Showable, hence that extends Showable.
  2. As was mentioned before, Rust uses impl Showable here because impl Showable is also the thing used when defining a typeclass instance for Showable (see here). In Scala, we use given at that spot; but we also chose to use given only for definition sites, and using for the corresponding use sites. So, if we want to follow Rust’s logic and be consistent with existing Scala syntax, the only correct choice here is using Showable.

Second, let me talk about the :-based syntax I used above:

def showAll(xs: List[? : Showable]): Unit

It was already mentioned that, while it looks fine when inside a type parameter list, it is very weird at the top-level:

def show(x: ? : Showable): Unit

is, unfortunately, quite bad. Moreover, it has the problem that ? cannot be meaningfully replaced by anything else. For example,

def showAll(xs: List[T : Showable]): Unit

doesn’t make sense. We are in a position where T must already be defined (it can’t be introduced as a new binding here). You might say we could write

def showAll[T](xs: List[T : Showable]): Unit

but that would be more clearly written as

def showAll[T : Showable](xs: List[T]): Unit

to begin with.

So I don’t think the ? : Showable is really viable. It would introduce inconsistencies.

Omitting the ? altogether, as List[: Showable], makes it even worse at the top-level, x: :Showable, no matter how we want to explain away that it makes sense.

In conclusion, I’m not quite set on the definitive syntax here, but my “least-bad” choice would be using Showable:

def showAll(xs: List[using Showable]): Unit
def show(x: using Showable): Unit

seems fine enough, consistent with existing Scala syntax, and even consistent with Rust’s idea (for those who like to take that as a good argument).

Now it might be a bit weird to explain that the desugaring chain

def showAll(xs: List[using Showable]): Unit // 'using'
// becomes
def showAll[T : Showable](xs: List[T]): Unit // ':'
// which then becomes
def showAll[T](xs: List[T])(using Showable[T]): Unit // back to 'using'

goes from using to : and back to using.

We may want to introduce using as an alternative to : when defining context bounds, to solve this path:

def showAll[T using Showable](xs: List[T]): Unit

may make sense. And, well, it would remove one magic use of the non-searchable :


OK, that’s all I had for today.

9 Likes

Makes sense to me. In particular, I think it’s a good sign that, midway through, I wound up going "Well, then you probably want a using alias for :" – and then found that you’d come to exactly the same conclusion. So this seems to be intuitively consistent…

I agree that show(x: ? : Showable) is a little ugly, and that’s why I originally didn’t propose something like it. But it’s really only a little ugly, and it is totally consistent with the rest of the language. In fact, it’s a little funny that I can’t write show(x: ?) but can write show(x: List[?]) – why force me to write Any?

But more importantly, I don’t think the worry about def showAll(xs: List[T : Showable]): Unit holds up – you can make the same objection about <::

def showAll(xs: List[T <: Showable]): Unit // not permitted
def showAll(xs: List[? <: Showable]): Unit // permitted
def showAll[T <: Showable](xs: List[T]): Unit // permitted.

Implementing the feature was actually pretty easy (you can see a first cut here). I think that’s a pretty strong indication that it already gels well with the language: I was able to combine the fact that type parsing already is okay with context bounds in some environments and is already okay with type bounds on ? in others.

Regarding using: I would be overall in favor of the change to the language if context bound syntax were changed, but I’m not sure that would be backwards compatible, since ? using Showable is infix sugar for using[?, Showable]. I think you’d need to make using a hard keyword, though I’m not sure.

I agree with the worries about introducing a type parameter. Once multiple type parameter lists are available, I hope this will be less of a concern. I’m actually not sure: when the compiler inserts a using clause for context bound sugar, does it append to the last using param list (if any) or make a new one? We should just follow whatever happens there.

Thanks for the detailed reply!

I just want to point out a couple things:

Proposed syntaxes with ? involved save no characters over just being able to introduce new type parameters while inside the argument list. E.g. def foo(x: T) instead of def foo[T](x: T). So, def foo(x: T : Show) would be better than bringing a ?.

But, allowing introduction of type parameters in the argument list, risks accidentally introducing a type parameter via a typo, when you wanted to refer to an existing one (for example, def foo(x: Thing, y: Tihng): Thing would no longer be an error, just incorrect)

Maybe it could work for the 90% case, if there was a constraint that only one type parameter can be introduced in that fashion and otherwise it’s an error. But still, I don’t think listing the type parameters up front is enough of a burden to make this worth it (even in the most egregious of shapeless Aux cases)

Perhaps stating the obvious, but you could again make the same argument for <: ( def showAll(xs: List[? <: Showable]): Unit) – and for that matter, any wildcard type parameter outside of case matches.

I think there are reasons to argue that context bound syntax should be fundamentally different from the existing type bound syntax (in particular, the fact that context bound syntax is sugar but <: is a compiler primitive), but I don’t know that characters saved is a good one.

And of course there’s no need to debate whether foo(x : T : Show) is better, since foo(x : T <: Show) has already been rejected.

@odersky @sjrd is there a path forward for this proposal? Several of the comments are negative, but after realizing how close the language already is to supporting this sugar, I find the arguments against this proposal (most of which focus on larger changes to the language) to be relatively unconvincing. Given that @odersky seems to be in favor, I’m unclear what to do next (since I believe the real SIP process is dormant). I have a PR available, but I’m not sure if I should just put it up now for approval, or try to follow some other feedback process.

(To be clear, I mean the updated proposal, which only proposes allowing ? : TL as sugar for a context bound on an anonymous type).

You may open a PR if you want. Otherwise, please be a bit more patient for a few more weeks or so, until we can fully put the SIP process back on track. We have been making active progress there in the past few weeks (and my going over all the recent pre-SIP is part of that effort). Sorry for the delay.

2 Likes

Great to hear. I opened a PR, but I don’t want to unduly rush the process – happy to wait if that’s preferred!

I personally feel that

  • the current syntactic sugar
    def showAll[S](using Showable[S])(xs: List[S]): Unit = xs.foreach(x => println(x.show))
    
    i.e. unnamed using clause is good enough (it scales well if you need to eventually name the parameter or if you want to use multiparameter type class, …), and
  • we don’t need any more syntactic sugar in this area and
  • even the context bounds ([S: Showable]) should be eventually dropped from the language to make it more regular.
4 Likes

As I said above, most folks in this thread who object to the proposal also largely object to context bounds in general, which is of course a reasonable position. After having seen how close the language already is to supporting the proposal, I feel like it’s hard to argue against its inclusion without taking the position that context bounds should be removed entirely, or changed to use impl or using or something similar.

The language is currently here

def foo[T• U] x: ? • U x: List[? • U]`
<: or >: :white_check_mark: :x: :white_check_mark:
: :white_check_mark: :x: :x:

and the proposal brings it to

def foo[T• U] x: ? • U x: List[? • U]`
<: or >: :white_check_mark: :x: :white_check_mark:
: :white_check_mark: :white_check_mark: :white_check_mark:

(and honestly I think we should make the final :x: go away in the name of regularity too, even if it’s a little meaningless).

I think the question of whether to change the : in the second row – or eliminate that row altogether – is orthogonal. Personally I see a good argument for changing the context bound syntax but not a good argument for removing it entirely.

Also note that

def showAll(xs: List[? <: Showable]): Unit

is already (effectively) sugar for

def showAll[S <: Showable](xs: List[S]): Unit

though of course the sugar is thicker for the context bound than the type bound.

3 Likes

I don’t mind context bounds, though if we were designing the language from scratch we would probably not add them in as they no longer pull their weight in terms of added complexity for saved syntax.

On the other hand, I find your proposal ugly, confusing, and unnecessary. It buys you almost nothing beyond saving a couple of characters. I especially don’t like the idea of implicitly introducing hidden type parameter lists.

2 Likes

Nevertheless impl traits seem to be very popular in Rust, even though one could also write them with an explicit type parameter there.

4 Likes

Lurker.
Proposed(?) inline type parameter syntax

def m(generic :T) = ???

looks extremely alarming to me. Typos is one reason, but a lack of an import is even worse.
If I type a name of a type in IDE and it does not complain, I will assume I already imported it in this file.

1 Like

Swift has also added this same feature, previously you would need a type parameter to a function (to capture the concrete type at the call site for a generic argument) and now you can avoid that by writing x: some Foo in the parameter list

I like the proposal of introducing the requirement of the type class at its “use site” because it seems to communicate better what the intention of the function is.

For example, I would read List[?: Showable] (or List[using Showable]`) as “a list of something that has a Showable”. It makes the code more direct for the human than the logic language of “for all T that has a Showable, a list of T” (or “for some T”, depending whether your are reading the interface or the implementation).

I have however faced more than once the desire to implement the problem described in this stack overflow question: scala - List of classes implementing a certain typeclass - Stack Overflow. Namely: I want a list of values, possibly of distinct types, all of which implement a certain type class. The goal, of course, is to use the respective type class instance with each value.

While it can be resolved with implicit conversions in Scala 2, it is brittle, hard to understand and slow to compile. I wonder if that syntax should not be reserved for the compiler to deal exactly with that problem, rather than to the proposed sugar.

1 Like