The status of `with` in Scala 3

A while ago the proposal was made replace inheriting multiple traits using with with using , . Partly because that is what other languages do. But mostly because with was considered as a keyword for the new implicit system.

However the new implicit system seems to have settled on using and given, so the last argument is not valid anymore. Also using a comma can be pretty confusing with tuples. The difference between a list of tuples and a list of items inheriting from two parents is much more difficult to distinguish.

trait A
trait B

// In Scala2
val myList: List[A with B]
val myList2: List[(A, B)]

// Currently in Dotty this looks more like a list containing an A and a B instead of a list of A with B
val myList: List[A, B]
val myList2: List[(A, B)]

Since Scala 3 seems to go towards using words instead of symbols (quite syntax, end blocks with the new optional braces, and the recently proposed as instead of @), this seems less consistent (and IMO) more confusing. So I am in favor of keeping it as is.

I can’t find any topic in Contributors where this was actively discussed, if I have missed something feel free to close it (and point me to the actual discussion).

2 Likes

I’m not familiar with the proposal to replace with with a comma, but IIRC, there was a proposal to distinguish between:

(1) cases where a type is extended and the order matters where a method implementation overrides another, i.e. after extends and in anonymous class instantiations

(2) other cases, where with is just a type union, such as in type ascriptions and as type argument, where the order does not matter

and for (2), it was proposed to replace A with B by A & B. I’m guessing for (1) only, we would replace it by A, B.

So, it would be:

class C extends A, B
val ab = new A, B { ... }
val list: List[A & B] = ...

val ab = c: A & B
3 Likes

For the moment, the dotty compiler accepts both , and with after extends. I argue that we should switch to , for the following reasons:

  • , is standard. There’s no need to be different for the sake of being different.
  • , reads better
  • , saves us a keyword; we’d no longer need with (after a transition period).

The one downside is that anonymous classes with multiple parents would be no longer supported. We cannot simply replace

new A with B { ... }

with

new A, B { ... }

without running into abiguities. On the other hand, that does not work in Java either, so one should ask oneself whether anonymous classes with multiple parents is a unique Scala signature feature that’s worth preserving. Note that we can always replace that idiom in Scala with

{ class C extends A, B { ... }; C() }

That’s a faithful translation; the anonymous class translates exactly into this.

2 Likes

This is a good point, I didn’t realize we would use & for the type. So my previous point is moot.

This is rather subjective and I disagree. Also the new quit syntax introduces then, so it seems rather arbitrary to replace with with a symbol and introducing then to replace parentheses. Generally Scala seems to favor words over symbols, and I personally think this is a good thing. Its slightly longer, but easier to type and easier to read, again IMO.

I think it is worth preserving. I find that we use anonymous classes quite often (mostly for stacking mixins or the ‘module pattern’) and removing it would introduce unnecessary boilerplate.

6 Likes

That’s not a faithful translation when A and B are JavaScript types in Scala.js. The original code directly instantiates A and creates additional fields and methods on the resulting instance, whereas the replacement creates a separate class, then instantiates that class. The semantics are different and are observable. Sometimes, the additional class can cause the object not to be accepted by a JavaScript library, which might for example throw a TypeError.

In those case, the only faithful translation is

new A { ... }.asInstanceOf[A & B]

which is not type-safe.

In either case, the “workarounds” are not nice.


Compare the argumentation in this thread to the one in https://github.com/lampepfl/dotty/issues/9829 about renaming @ into as, and the alternative proposal to use something with the semantics of &. In that thread, you argue that the elegant & solution cannot be considered because it prevents the use case

val (_, Captured @ _, NoInits @ _) = newFlags(32, "<captured>", "<noinits>")

That one also has a workaround, namely:

private val (_, captured @ _, noInits @ _) = newFlags(32, "<captured>", "<noinits>")
val Captured = captured
val NoInits = noInits

Is that pattern more worthy than the anonymous class with two parents? Is the workaround uglier? What makes this pattern worth blocking a semantically elegant improvement, and the anonymous class with two parents pattern not worthy of blocking a purely aesthetic change?

1 Like

That’s not a faithful translation when A and B are JavaScript types in Scala.js.

That translation is done in desugaring, before typer even looks at it. Are you saying that step is overridden in Scala.js? How?

1 Like

What if we allowed new (A, B){} to satisfy the multiple-parent-anonymous class use case? after all Tuple types are final and cannot be subclassed, and even if they could we could tell people to do new Tuple2[A, B]{} instead if they really want to extend tuples

I find that new (A, B){} looks a bit awkward. If we must keep multi-parent anonymous classes, another possibility is to stick with the status quo, and keep with as an alternative to ,. The downside would be multiple ways of doing things.

On the other hand, since “,” is shorter, easier to read, and more standard, I would expect most people to change over to it, over time.

So it’s equivalent in Scala, but not in Scala.js? So, if I’m using Scala.js, innocent refactorings such as replacing an anonymous class by a non-anonymous one may change the behavior of my code? That sounds worse to me than lack of type safety.

I agree that new (A, B){} is a bit awkward, but I personally instantiate anonymous multi-parent classes rarely enough that I think it’s worth the tradeoff if it lets us properly kill with and helps us standardize on ,

Again, I don’t think this is universal truth, and also a bit inconsistent with the other changes coming in Scala 3 (as, if then else, end, do).

On the other hand, this syntax bike-shedding will never make everybody happy. So I’ll take my loss here. When a choice is made I hope that it is consistent across all use cases. Thus either , everywhere, or with everywhere.

1 Like

I think the word class itself has wrong semantics for something that only appears once. object C extends A, B { } would be better.

In both cases the name is superfluous. Naming is hard, and names here might end up like TheThing or AandB. This decreases readability, as someone reading the code might ask “what does this name mean? why is it named this?” and having to spend more mental capacity to make sure they understand the intention of the code. object _ extends A, B { } might work but is more cryptic.

The suggested new (A, B) { } is elegant IMO.

1 Like

Yes, it is undone by Scala.js. It is done by reversing the transformation in the back-end, both in Scala 2 and in Scala 3.

For more context, rationale, and implementation strategy, you can look at the Scala.js language proposal that introduced this behavior: https://github.com/scala-js/scala-js/issues/2009

It is equivalent in Scala.js as well for Scala classes, i.e., classes that extend AnyRef or AnyVal. For those, the behavior will be the same in both cases. This is of course necessary to keep portability with Scala/JVM.

It is different for JavaScript classes, i.e., classes that extend js.Any. And for those, equivalence with JavaScript semantics is paramount (that is the very purpose of JS types). In that case the “refactoring” is not a refactoring. It is a semantic change.

Why can’t we use & in extends?

All along we were told that we needed two separate type operators because & has different semantics than with. The order of types does not matter with &, however with does trait linearization so the order matters.

But if we’re not retaining with as a type operator, and if extends uses commas, then that’s just syntax. In other words the part after extends is no longer a single type expression involving with, it’s simply a list of types to extend, then why not use &? It’s true that order in extends matters but where’s the pitfall exactly?

If we don’t need with then why can’t we just use & everywhere?

3 Likes

If we say & is commutative then it’s awkward to say “except” when it follows an extends. But I agree it is a possible option nevertheless.

Another alternative is to require that a multi-parent anonymous class appears itself in braces, like this:

   { new A, B { ...  } }

Then it would be unambiguous since a comma cannot be a top-level separator for expressions inside braces.

1 Like

As someone that writes Scala some of the time, I’m not really a fan of something like { new A, B { ... } } or new (A, B) { }. I think that I would find these both a bit cryptic and perhaps too easy to confuse with other things (e.g., tuples). In that sense I think that using “with” is better for users less familiar with the language.

Also, if anonymous classes with multiple parents is not really used very much then I would probably be in favour of just deprecating it. Each feature removed from a language makes it slightly less complicated.

For me, I mentally think of “with” as adding in a trait (or traits), and I find it strange that a class can “extend” a trait. I can see that a class can extend a class (and mix in traits) and a trait can extend a trait (or traits), but it feels strange for a class to extend a trait rather that implement or use the trait.

So, without thinking about this too deeply, I guess that I would ideally like a syntax something like:

trait T1
trait T2
trait T3 extends T1
trait T4 extends T1, T2
    // Or perhaps trait T4 extends T1 with T2, e.g. if T2 was a super trait

class C1
class C2 extends C1
class C3 extends C1 with T1, T2
class C4 with T1, T2 // Not allowed today.

new C1 with T1 {}
new C2 with T1, T2 {}

Not sure if this input is helpful, but perhaps a different perspective …

We’d have the same ambiguity as explained above.


I don’t think using colons is a good idea, but I recognize it does make multi-line extension nicer (if we allow trailing commas):

class Foo extends
  Aaa,
  Bbb,
  Ccc,
  Ddd,

instead of the awkward:

class Foo extends
  Aaa with
  Bbb with
  Ccc with
  Ddd

or the annoying space-fiddler:

class Foo
  extends Aaa
     with Bbb
     with Ccc
     with Ddd

Then perhaps
new C2 with (T1, T2) {} \\ or
new (C2 with T1, T2) {}

I.e., I think that having the ‘with’ gives a better clue about what is going on, and is also easier to search potential documentation for.

When I look at languages that I’m no longer familiar with then some of them have many symbols that make it very hard to get any sort of idea about what is going on (e.g., possibly some of the stuff added more recently to C++).

If we are using & where the order doesn’t matter (i.e. as type ascription or type argument), maybe we can use :& where the order does potentially matter (i.e. after extends or in anonymous class instantiations).

I think that’s intuitive: it’s clear that & and :& are similar, that & is commutative (because it usually means bitwise AND) and :& is not commutative (because operators with preceeding colons are usually not commutative).