Skip to content

Conversation

hamzaremmal
Copy link
Member

@hamzaremmal hamzaremmal commented Aug 24, 2025

TODO:

  • This will require a SIP to get to the language
  • Add support for case .some(v) => ???, case _: .InnerClass, ...
  • Add error message when the expected type cannot derive the qualifier
  • Specify the qualifier resolution (which should answer any question we might have)
  • Add experimental language import for the feature

@hamzaremmal hamzaremmal self-assigned this Aug 24, 2025
@hamzaremmal hamzaremmal added area:experimental needs-sip A SIP needs to be raised to move this issue/PR along. labels Aug 24, 2025
@hamzaremmal
Copy link
Member Author

The CI fails as we cannot bootstrap. The following snippet doesn't represent chained applications at the moment, but rather two expressions.

a.foo(0) // first expression
 .bar(1) // second expression (unqualified selection)

@odersky
Copy link
Contributor

odersky commented Aug 26, 2025

I have thought some more about it, and tried to nail down my unease with the proposal. I think it is this:

.Red syntax is syntactically weird but shorter than Color.Red. This means code writers will use it and most likely abuse it, whereas code readers will hate it.

Also, there is the problem as you noted that

a.b
.c

is valid code today and we need to keep it.

So, as a counter-proposal maybe we should restrict it to enums and drop the leading . altogether? The rule would be:

When resolving an unqualified identifier, if all other schemes have failed, check whether the target type is an enum. If yes, look up the identifier among the cases of that enum.

That would let us write simply Red whenever the target type is Color. It would work in both patterns and expressions.

Why single out enumerations? They define a lot of names that are usually easily recognizable and where the nesting is forced on us. An export Color.* following the enum works sort of, but also exports all the auxiliary structures that the developer and the compiler put in an enum companion, so it does too much. So it seems useful and harmless to not requite the prefix.

On the other hand, if we allowed this everywhere it would essentially allow full scope injection which is very powerful and can quickly lead to incomprehensible code.

@kyouko-taiga
Copy link
Contributor

The use of elided qualification goes way beyond just enum cases in Swift and I do believe the feature makes sense in Scala too, pushing in the idea of promoting concision.

The general rule in Swift is that if there's an expression of the form f(x.y.z) then x can be elided iff:

  • y.z is any expression that computes the expected type for x.y.z; and
  • y is a symbol defined in the static scope of that expect type (roughly the scope of the companion object in Scala).

The constructors of an enum in Swift satisfy these requirements because this code:

enum Color {
  case rgb(Int, Int, Int)
  case hsv(Int, Int, Int)
}

is roughly equivalent to this:

sealed trait Color

object Color:
  case class RGB(r: Int, g: Int: b: Int) extends Color
  def rgb(r: Int, g: Int: b: Int): Color = RGB(r, g, b)
 
  case class HSV(h: Int, s: Int: v: Int) extends Color
  def hsv(h: Int, s: Int: v: Int): Color = HSV(h, s, v)

So when we write val x: Color = .rgb(10, 10, 30) we have indeed an expression that computes an instance of Color applying a symbol defined in the companion object of Color.

It is easy to see how this technique generalizes to other use cases. For example, any constructor defined in a companion object can be applied that way. In Swift, that can prove particularly useful in code that sets up many instances.

// Library code

struct Vector2 {
  let x, y: Double
  static let zero = Vector2(x: 0, y: 0)
  static let unitX = Vector2(x: 1, y: 0)
  static func + (lhs: Self, rhs: Self) -> Self { ... }
}

struct Circle {
  let origin: Vector2
  let radius: Double
  static func enclosing(_ cs: [Circle]) -> Circle { ... }
}

typealias VennDiagram = Array<(Circle, String)>

// User code

let c = Circle(origin: .zero, radius: .pi)
let d = Circle(origin: . unitX, radius: .pi * 2)
let xs: VennDiagram = [
  (c, "A"),
  (.enclosing([c, d]), "B")
]

Another advantage is that companion objects become a very convenient "natural" namespace, which can be leveraged to create more meaningful helper types. It is not rare in Swift to write things like Graph.Edge.Label where all three names are types.

One can certainly write very similar code in Scala using clever combinations of imports and exports but

  • imports can be verbose and may introduce names in unnecessary large scapes;
  • exports may needlessly pollute the namespace of the user who doesn't need some or any of the exported symbols.

Clearly in the above example no one wants to import offset in the global scope just to write offset(x, Vector2.unitX) yet this name is perfectly sensical in the static scope of Circle. The alternative in Scala is to require the qualification, which is often pure noise in cases where the expected type is completely unambiguous. The same argument applies to unitX, which is a reasonable name in the context of Vector2 but would also in a Vector3 class that could be imported in the same scope. IMO, nothing beats the concision and clarity of an expression like myVector.scaled(by: .unitX * 2). I can certainly point to real production code demonstrating such a situation.

Similarly, .pi * 2 is a perfectly sensical expression but should we have more than a single floating-point type in scope (a reasonable use case in a Scala Native application) we'd have to either write Double.pi or arbitrarily decide that pi has type Double in our entire scope. Ditto for any other common constant.

.Red syntax is syntactically weird but shorter than Color.Red. This means code writers will use it and most likely abuse it, whereas code readers will hate it.

This observation is rather subjective and somewhat disproved by empirical evidence in Swift.

So it seems useful and harmless to not requite the prefix.

Doing so would discourage certain case names when those could clash with other similarly named types or enum cases. For example:

enum TypedTree:
  case App(lhs: TypedTree, rhs: TypedTree)
  case Abs(x: String, t: Type, e: TypedTree)
  case Var(x: String)

enum Tree:
  case App(lhs: Tree, rhs: Tree)
  case Abs(x: String, e: Tree)
  case Var(x: String)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:experimental needs-sip A SIP needs to be raised to move this issue/PR along.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants