enum Tree
case Leaf(x: Int)
case Node(left: Tree, right: Tree)
val t: Tree = Node(Leaf(1), Leaf(2))
// aka
val t: Tree = ..Node(..Leaf(1), ..Leaf(2))
// fairly unambiguous because all expected types are the same
enum RedTree
case Leaf(x: Int)
case Node(left: BlackTree, right: BlackTree)
enum BlackTree
case Leaf(x: Int)
case Node(left: RedTree, right: RedTree)
val t: Tree = Node(Leaf(1), Leaf(2))
// aka
val t: Tree = ..Node(..Leaf(1), ..Leaf(2))
// Here we want enum classes from the inner point
enum Tree
case Leaf(x: Int)
case Node(left: Tree, right: Tree)
extension [T](x: T)
def debug(s: String): T =
print(s"$s: $x")
x
// expectation: If an expression is valid,
// slapping a `debug` on it won't make it invalid
val t: Tree = Leaf(1) // valid
val t: Tree = Leaf(1).debug("t") // valid ?
// aka
val t: A = ..Leaf(1) // valid
val t: A = ..Leaf(1).debug("t") // valid ?
// Here we want the expected type of the whole expression to drive the relative scoping
// This might still work with expected type, but I'm fairly certain that there can be examples where it's not the case
Oh and we should probably also do something smart with unions:
val t1: Tree = Leaf(1)
val t2: Tree | Null = Leaf(1) // should also work
// aka
val t1: Tree = ..Leaf(1)
val t2: Tree | Null = ..Leaf(1) // should also work
I agree that if this feature is added, we should explicitly handle nullable types where explicit-null is enabled. But other than that, it will only work for concrete types and not a union of concrete types. Too many things can be ambiguous and confusing if we enable that.
I agree with Martin that .. is not so nice. For a beginner it is cryptic.
I think this is much better
def changeTrafficLight(current: Color): Color = current match
case Red => Yellow
case Yellow => Green
case Green => Red
than this
def changeTrafficLight(current: Color): Color = current match
case ..Red => ..Yellow
case ..Yellow => ..Green
case ..Green => ..Red
We have the target Color type. We use documentation. If Color wasnât an enum but defined as a sealed trait with case objects outside of the companion they would show up (as we could see in the docs and IDE hints).
If some of the counter-examples above had explicit return type it would be more reasonable to assume that exported members are in-scope.
I think the opt-in export modifier is fine. We use documentation. We use IDE support. We will not need to remember everything. We can always ourself shape the scope with imports and exports in wrappers. It is reasonable that the api designer is in control of the api and the scope after wild-card imports.
Companion objects provide factories and givens - so in that sense there is regularity. So Iâm on the same page as @odersky and Iâm not convinced that the export modifier would be a hassle, and I think it is an elegant semantics for solving the enum inconvenience.
I agree with your reasoning given the premises. Iâm not sure the premises are correct, however, so this may need to be revisited.
However, I do readily agree that no-extra-syntax-at-all is superior to minimal-syntax-that-still-looks-visually-somewhat-jarring.
Is this actually the same situation, though?
Firstly, with infix, why is it a good thing for the community? What practical impact does it actually have? Doesnât it primarily just make people stop using infix notation, so that people who donât like infix donât have to put up with code written by people who do?
For me itâs just been an exercise in tediously inserting infix into my own code, writing clearly less readable code because the library didnât have infix on for whatever reason even though itâs a symmetric binary operator like intersect, and adding the âignore infix warningsâ to every file. I am constantly very tempted to just write less clear code and stop using infix because the feature makes it painful. I have exactly zero times benefited from it in any way whatsoever as someone who actually uses the feature.
However, I can understand that having less infix can, indeed, make things more regular; and some people weight syntactic simplicity and regularity more heavily than clarity-once-learned, and so I accept that Iâm part of the community for which it âclashed with preferred coding stylesâ. But the solution has been to make infix use rare, hasnât it? Even in cases where the library designer could have planned for the possibility but didnât. Even in cases where the library designer did plan for the possibility but you know it might not work so you donât check every method to see whether it will work or not.
Is this the kind of âvery good thingâ you envision for export is a class modifier in an object? That is, it should be rare, because not only do you have to plan for it, the type of planning that it takes is a maintenance burden (you have to think through every item, and then think it through again if you change the API)?
If the answer is âyes!â, then fair enough.
If weâre going with export and no modifiers, though, can you export an export, in the two different senses?
trait Thing {}
object Thing:
export case class AThing(i: Int) extends Thing {} // Proposed
trait Subthing extends Thing {}
object Subthing:
case class TheThing(s: String) extends Subthing {}
export Subthing.TheThing // This is valid
export export Subthing.TheThing // Um...?
Iâm not sure this is the right level of granularity. There are a lot of things isomorphic to nullability. For example, here are three ways to create a Json hierarchy where you have both a with-error and a without-error hierarchy:
// Encoding 1
case class JErr(msg: String) {}
trait Json:
def range: (Long, Int)
object Json:
export case class Str(s: String)(val range: (Long, Int))
extends Json {}
type Js = Json | JErr
// Encoding 2
trait Js {}
case class JErr(msg: String) extends Js {}
trait Json extends Js:
def range: (Long, Int)
object Json:
export case class Str(s: String)(val range: (Long, Int))
extends Json {}
// Encoding 3
trait Js {}
object Js:
trait Json extends Js:
def range: (Long, Int)
case class Err(msg: String) extends Js {}
export case class Str(s: String)(val range: (Long, Int))
extends Json {}
As proposed, I think that none of the hierarchies allow the short form on things typed both Js and Json, admitting short-form matches on correct but not error cases, even though all of them nominally are equally good candidates for it to work. Whether we have no decorator or a .. is, I think, orthogonal to this issue. Rather, this is more of a concern about going beyond enums at all, because once you have as-you-please assembly of subtypes, itâs very hard to get out of âambiguous and confusingâ.
Agreed. Especially for enum, where it is so unambiguous what is meant, the dots seem like unnecessary and visually unpleasant clutter. However, columnar alignment
def changeTrafficLight(current: Color): Color = current match
case ..Red => ..Yellow
case ..Yellow => ..Green
case ..Green => ..Red
makes it look a lot better. Columnar alignment makes a lot of things look better, but this example was chosen presumably to be a near-worst-case (and I agree itâs pretty bad) and at least to my eye itâs mostly okay once aligned. (Even if dotless is still cleaner, including when aligned.)
Needing to refer to documentation slows things down, so that is not an argument in favor. Nobody was saying that it wasnât even discoverable, were they? It just makes it harder to gain fluency with libraries because you have more to learn.
And not everyone uses IDEs with that level of richness. In practice, some people do actually remember everything. Making the decision that âremembering everything is not a supported way to use Scalaâ is actually a pretty big thing, I think.
Furthermore, this presumes that the relevant IDEs are going to grok this new feature fast enough and display it helpfully to the user. Hover is easy enough (the presentation compiler can give the type), but the tricky part is autocomplete, where you need it to offer Heading when itâs typed HTMLElement. Otherwise, you type HTMLElement. and then you get Heading as an autocomplete, which has completely bypassed the feature.
So while I agree that it is possible to lean on IDEs for help with some trickier parts of language features, I donât think this alleviates the concern much at all.
I propose that if we want this in sooner rather than later, we limit to enums for now, whether itâs with .. or bare; otherwise we need to tackle the use cases for people who are not using enums because they want something more complicated that isnât supported by enumsâbut that will often break export too, so the use cases covered by explicit export but not enum are pretty thin.
(Enums can have companion objects and the companion can be extended with extension methods, so itâs not like you need a place to hang helper methods/classes. You need not-Enum because youâre doing something tricky e.g. using inheritance rather than composition in a way that enum doesnât support, or nesting in a way that enum doesnât support.)
If we limit it to enums, we can defer the sigil decision, because even if enums go in with nothing, we can always allow .. notation for them if we decide that expanding the feature is a good idea but only if .. is cool. Extra syntactic sugar for enums should be fine because theyâre an extra-clear case (e.g. with lambdas we have extra-short forms for the most straightforward cases).
I donât really see any significant potential for confusion here. When two enums have cases of the same name, we can simply print an error to that effect. This matches the behaviour we already have today when we use imports:
enum Foo:
case Bar(i: Int)
enum Baz:
case Bar(s: String)
import Foo.*, Bla.*
Bar("foo")
-- [E049] Reference Error: -----------------------------------------------------
1 |Bar("foo")
|^^^
|Reference to Bar is ambiguous.
|It is both imported by import Foo._
|and imported subsequently by import Baz._
def changeTrafficLight(current: Color): Color = current match
case Color.Red => Color.Yellow
case Color.Yellow => Color.Green
case Color.Green => Color.Red
This is annoyingly repetitive and we want to fix that. Thatâs the main objective. Every solution has to be measured by how well it addresses this objective and everything else is a bonus feature .
Agree. Let me bring a frontend/Scala.js perspective to the discussion. In UI programming, we constantly work with patterns like:
The call site becomes shorter and cleaner â but it still requires multiple imports that exist solely to make this syntax nicer.
From this angle, a language feature that lets us access members of a âtarget typeâ (such as enum cases or nested objects) without manually importing them would be a huge improvement. If the compiler can already infer the expected type at the call site, it feels natural that it could also look up the relevant members automatically.
To me, achieving that would be a real win for ergonomics.
Yes, I think infix and export would only be used infrequently. For export itâs âI really wish this could be an enum, but it needs to stay an open sumâ. I am also fine with delaying export and restring this initially to enums only, until we see more convincing usage examples.
I know that the infix change did upset some peopleâs well-reasoned conventions. The problem was that the code I saw in the wild did not have clear and uniform conventions when to use infix, so the code was typically all over the place. For intersect and symmetric operations like it, thereâs always the possibility to write them infix in backquotes. Did you try that? For me personally, I have found them to work well enough.
No, export as a clause does not allow any modifiers.
OK, so going back to the original example: An api-side export takes us to a bit less verbosity:
// This is real Scala 3.7.4
final case class Shape(geometry : Shape.Geometry, color : Shape.Color)
object Shape:
export Geometry.*, Color.*
sealed trait Geometry
object Geometry:
case object Triangle extends Geometry
case object Rectangle extends Geometry
case object Circle extends Geometry
end Geometry
sealed trait Color
object Color:
case object Red extends Color
case object Green extends Color
case object Blue extends Color
end Color
end Shape
You then can:
val redCircle = Shape(Shape.Circle, Shape.Red)
But we would like to:
val redCircle = Shape(Circle, Red)
The above could be achieved with further exports on top level of the package but then the namespace outside of Shape is âpollutedâ.
A proposal is that we could achieve this by allowing export in new positions to unlock target-type scope injection:
// this is hypothetical Scala
final case class Shape(geometry : Shape.Geometry, color : Shape.Color)
object Shape:
sealed trait Geometry
object Geometry:
export case object Triangle extends Geometry
export case object Rectangle extends Geometry
export case object Circle extends Geometry
end Geometry
sealed trait Color
object Color:
export case object Red extends Color
export case object Green extends Color
export case object Blue extends Color
end Color
end Shape
The name space is not âpollutedâ and target-typing-based member selection works outside Shape where Shape stuff is expected like so and the api-designer can make a nice api:
val g: Shape.Geometry = Circle
This possibility would be discoverable in the api-docs by the export modifier. IDE-support could show hints and make completion etc.
@soronpo What do you think about that? Would it solve your original use case?
Yes, but that lost much of the visual clarity (at least to me) while adding a third way to do the same thing, so it was even worse.
a plus (b times c) // Very clear
a `plus` (b `times` c) // A bit cluttery
a.plus(b.times(c)) // A bit more cluttery
(a plus (b times c)).foo // Downside: needs parens to call methods
(a `plus` (b `times` c)).foo // Still has downside and is a bit cluttery
a.plus(b.times(c)).foo // Avoids downside at least!
For a feature that is all about syntactic elegance at the expense of regularity, backticks arenât for me a winning tradeoff except in very rare cases.
I guess the question I have is whether that is worth an extra language feature, given that you can accomplish a similar thing with composition: just make the open part a parameter of one of the cases; and given that export as envisioned doesnât easily support nesting.
Allowing method- and class-level export permits design flexibility that admits usage that is confusing to the user (incomplete coverage of export, method- rather than type- or language-construct- memorization of âwhat worksâ). It is potentially helpful, but the cost in learning and potential irregularity is very high compared to âenums have this cool trickâ.
But they donât pop out of nowhere, theyâre related to the type of the object you need to create. Itâs not that different from calling a method on an object: when you do that, you can refer to symbols defined in that objectâs type without naming that type again: itâs "foo".indexOf("oo"), not "foo".String#indexOf("oo"), because that would clearly be very tedious. And yet we insist on that tedium when creating objectsâŚ
This oneâs actually pretty simple: local variables must take precedence because there is no way to refer to them explicitly, whereas there is an explicit way to refer to enum cases.
Besides, for compatibility reasons we would probably want enum cases to have the lowest priority anyway. Just do a lookup like we would today, and if we donât find anything, consider the enum cases. Itâs possible to make it smarter than that (e. g. apply disambiguation rules similar to those used for method overloading), but for starters we should probably just do the simple thing and see how far it gets us.
I also think that there is not really a serious issue with ambiguity; The rule that unimported identifiers can pop up so long as they are cases of a known target type is easy to understand and immediately memorize. We can get this unqualified accessing today using imports, and letâs not pretend that people actually read imports at the top most of the time.
It is rare that you are going to accidentally shadow class names in the same file anyway, theyâre practically always uppercased, and we already allow shadowing in the language with the assumption that programmers will handle it sensibly.
I think the general lookup without any leading sigil or export modifier would be best if possible to implement without conflicting with pre-existing features, and I wouldnât intentionally limit the feature just because we are worried some people writing some questionable code might accidentally shadow and then have to fix it.
We should discuss how this feature is going to interact with into. At a minimum, when the expected type of an expression is into[A], where A is an enum type, the cases of that enum type must be considered.
enum E1:
case X
def f(e: into[E1]) = ()
f(X) // this must compile
And how about this?
enum E1:
case X
enum E2:
case Y
given Conversion[E1, E2] =
case E1.X => E2.Y
def f(e: into[E2]) = ()
f(X)
I tend to think this should work too. Otherwise itâs another one of those situations where two features work in isolation but it breaks when you try to use them together, and that always sucks.
Another case would be this:
enum E1:
case X
enum E2:
case X // note: case is named the same
given Conversion[E1, E2] =
case E1.X => E2.X
def f(e: into[E2]) = ()
f(X) // is this ambiguous?
Iâm a bit on the fence about this one, but I think the ambiguity can reasonably be resolved by prioritizing E2.X because it doesnât require a conversion whereas E1.X does.
And this case is obviously ambiguous:
enum E1:
case X
enum E2:
case X
enum E3:
case Y // note: different name
given Conversion[E1, E3] =
case E1.X => E3.Y
given Conversion[E2, E2] =
case E2.X => E3.Y
def f(e: into[E3]) = ()
f(X) // obviously ambiguous
Another question I have about this feature: is it going to work for sealed types, and if so, how exactly? I think it should: any non-abstract class or object derived from the sealed type within the same file should be available through this mechanism. sealed types still have features that enum types donât, like an abstract method in the sealed type that is implemented in its subtypes.
There are also multi-level enums to consider:
(They are not an accepted SIP yet, but itâs hard to argue either proposal is so important that breaking the other is irrelevant, see forum, SIP)
enum Animal:
case enum Mammal:
case Dog, Cat
case enum Bird:
case Sparrow, Pinguin
def foo(x: Animal) = ???
foo(Animal.Mammal.Dog) // Allowed
foo(Mammal.Dog) // Allowed ?
foo(Dog) // Allowed ?
This can be somewhat solved with .. (but itâs very ugly):
enum Animal:
case enum Mammal:
case Dog, Cat
case enum Bird:
case Sparrow, Pinguin
def foo(x: Animal) = ???
foo(Animal.Mammal.Dog) // Allowed
foo(..Mammal.Dog) // Allowed
foo(....Dog) // Allowed ?
// or the following, but it makes less sense
foo(..Dog) // Allowed ?
Maybe # is superior in this regard:
enum Animal:
case enum Mammal:
case Dog, Cat
case enum Bird:
case Sparrow, Pinguin
def foo(x: Animal) = ???
foo(Animal.Mammal.Dog) // Allowed
foo(#Mammal.Dog) // Allowed
foo(##Dog) // Allowed ?
// or the more sensible
foo(#.Mammal.Dog) // Allowed
foo(#.#.Dog) // Allowed ?
Itâs not related to multi-level enums, but what values will be seen in scope- the entire sealed cases chain or just what is found in the target typeâs companion. Iâm inclined to support the latter, because itâs a simple rule that reduces ambiguity (e.g., what happens when two internal hierarchies have the same case name).
I will just say that I will never propose or accept a multi-token as a hierarchy indication (## or âŚ.)
Could you give an example of what ambiguity you want to avoid here?
I can only see something like this:
sealed trait Foo
object Foo:
case object Bar extends Foo
object Bla:
case object Bar extends Foo
val _: Foo = Bar
And all I can say is that 1) you shouldnât be writing that kind of stuff in the first place, and 2) that absolutely should give an ambiguity error, and not disambiguate in favor of Foo.Bar.
No, # (as I proposed it) is an expression that refers to the companion object of the expected type, similar to how _ is an expression that refers to the parameter of a lambda expression. Therefore you canât use # after a period, just like you canât use _ after a period.
For multi-level enums I would suggest making all cases available, i. e. foo(Dog), foo(Sparrow) etc. should all just work. I think it would be quite questionable to design an enum type where different cases have the same unqualified name, and nudging people away from that by giving them ambiguity errors is probably not a terrible thing. If they really want to do it, they can always spell it out completely, and those who write their enums in a sensible way are rewarded with greater convenience.
Nope, I wholeheartedly disagree. Itâs not favoring one over the other. There is only one choice, the object thatâs in the companion of Foo (which may need to be marked with export).
Could you elaborate? Why is Foo.Bar the correct choice to make here, even though Bla.Bar has both the same unqualified name and the correct type, just like Foo.Bar does? IMNSHO this is begging for confusion.
Yes, certainly, sorry I wasnât clear enough. IMO, if this proposal moves forward, the objects being considered for relative scoping only belong to the target typeâs companion, not all extensions of that type. Thatâs the most simple rule and is useful enough, IMO.