Relative scoping for hierarchical ADT arguments

To avoid namespace pollution, I like to declare ADTs in hierarchies like so:

final case class Shape(geometry : Shape.Geometry, color : Shape.Color) 
object Shape {
  sealed trait Geometry
  object Geometry {
    case object Triangle extends Geometry
    case object Rectangle extends Geometry
    case object Circle extends Geometry
  }
  sealed trait Color
  object Color {
    case object Red extends Color
    case object Green extends Color
    case object Blue extends Color
  }
}

The problem is that to construct a new object from this composition can be rather verbose, depends on how deep the hierarchy goes.

val redCircle = Shape(Shape.Geometry.Circle, Shape.Color.Red)

I can of course use import to reduce the verbosity, but that recreates the namespace pollution I was trying to avoid.

I would like to propose a way to refer to an object/type in relation to the expected ADT argument. Using the example above:

val redCircle = Shape(.Circle, .Red)

In pattern matching:

val supportedShapes = shape match {
  case Shape(.Triangle | .Rectangle, _) => true
  case _ => false
}

In regards to syntax, if .Arg is not obvious enough we can also consider a using a concept similar to paths: ./Triangle or $.Trianlge, or anything else that would make it more clear.

What do you think?

This is taken straight from Swift, btw. However, as written, I think it may not have enough benefit for the complexity it adds to both teaching and the language implementation, and also it is very specific and lacks generalization to other related problems. (This adds full-blown TDNR for top-level selections [not methods as with extension methods/conversions], but only based on return type and only for enums – what about related problems like scope injection?)

1 Like

I never coded in Swift, so I didn’t know. Thanks.
Here is the related section from Swift https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html

I wouldn’t even bother with the dot. What is the point of it? In particular, if you have

trait T {}
object T {
  val a = new T {}
  object B extends T {}
  def c(): T = new T {}
}

def f(t: T) = ???

then f( ) automatically imports the companion object scope, so f(a) and f(B) and f(c()) just work. There are approximately zero downsides for this. If instead of being explicit, a, B, and c() were implicits (givens in dotty), they’d be found. So why not find them if something explicit is requested?

To keep it from getting unwieldy, we could restrict it to only sealed traits or classes (or enums, in dotty).

Then you just have

val redCircle = Shape(Circle, Red)

with no extra fuss. The dot seems unnecessary extra clutter. The chance for confusion is minimal. Linting tools could catch the case where the auto-import introduced an ambiguity. For example, if you have

def environment(light: Light, water: Water) {}
sealed trait Light {}
object Light {
  object Off extends Light {}
  object On extends Light {}
}
sealed trait Water {}
object Water {
  object Off extends Water {}
  object On extends Water {}
}

then environment(Off, On) throws away all the safety beyond just having a boolean. So linters could catch that and say something like, "ambiguity in inferred import: Light.Off imported, but not distinguishable from Water.Off".

I don’t think the generality problem that @kai mentions is actually much of a problem. Getting nice syntactic sugar for general classes of problems can require undue effort from the compiler. But this precise problem is already solved in a more difficult form for implicits, so in principle it should be achievable. (Whether the compiler architecture makes it easy enough to implement I do not know.)


Specifically, the rule I’d favor is

  1. For any expression where the type is known (e.g. because of type ascription or method parameter), and
  2. That type is a sealed trait or class (or an enum in dotty), and
  3. That trait or class has a companion object, then
  4. Every symbol in that companion with a type that is a subtype of the sealed trait or class is visible in this expression.

So you could do stuff like

val c: Shape.Color = Red
{ println(Square); Rectangle }: Shape.Geometry

as well. (The latter perhaps being questionable form, but I would admit it as a necessary consequence of simple lookup rules.)

2 Likes

It is true that we don’t need the extra ., but I was afraid of ambiguity. I didn’t consider the similarity with implicit resolution. Nice observation! So as long as the compiler can properly report ambiguity (and not allow shadowing) I’m in favor of your “dotless” proposal.

https://contributors.scala-lang.org/t/proposal-for-multi-level-enums/3344/26

refs to this one.

1 Like

@odersky I would love to get your take on this (more specifically @Ichoran’s modified proposal). Of course, I’m not aiming for Scala 3.0.

I see the usefulness! Some thoughts:

  • Not using a prefix dot looks a lot more natural
  • But the result would be general scope injection, which has great potential for good as well as bad, so it’s exciting and scary at the same time.
  • Restricting it to constructors could tame the abuses but looks a bit ad hoc to me.

So… not sure. Right now I am concentrating on 3.0, without looking too much at what could come afterwards.

3 Likes