Relative scoping for hierarchical ADT arguments

I think this will be forbidden grammatically. But generally I see leading . as a syntactic sugar for a full object path. So basically anything that can be done with somewhere.far.away.Type. can be just with . given there is a destination type with a companion object. That also includes extension methods or Selectable fields if the companion supports those.

1 Like

The expected type idea makes a lot of sense in my head, it has the benefit(?) of allowing the following:

val l: List[T] = ???
val m: Map[Int, T] = .from(l)

And then you develop a taste for it and you end up disliking the way Scala typically does manual conversion, i.e. by defining a method in the source rather than in the companion of the target :wink:

This is a bit unusual, but is actually pretty nice

In general, we encourage people to annotate types. e.g. types for implicits, types for public members, and so on.

However, that means we are basically giving up type inference: rather than val m = Map.from(l), you get val m: Map[Int, T] = Map.from(l). There is duplication here, boilerplate.

With the proposed change, you get val m: Map[Int, T] = .from(l). The duplication and boilerplate is gone. The public or implicit member has an explicit type signature.

This solves a long-standing conundrum with Scala: best practice says explicit types, best practice says no duplication. Previously this was unsolvable, but Swift’s leading dot provides a solution with the best of both worlds

4 Likes

Would welcome suggestions for the purpose of the SIP.

Brainstorming:

  • Shortcut period
  • Companion inference
  • Companion object ellision
  • Expected-type guided qualification
  • Short qualification
  • Dotful syntax (awful name)

How does Swift call it ?

Devil’s advocate
One could say the same about .():

val loop3: Map[Int, T] = Map(0 -> 1, 1 -> 2, 2 -> 0)

Duplicates Map !
We should therefore favor:

val loop3: Map[Int, T] = .(0 -> 1, 1 -> 2, 2 -> 0)

I honestly think for variables like this it’s not so bad, but for methods, I think it makes the call-site less clear:

foo(.(0 -> 1, 1 -> 2, 2 -> 0))

This also applies to it normally actually:

keys(.from(l))

What does my list get transformed into ?

.() looks weird for syntactic reasons, e.g. because Map.() is not allowed, but eliding the Map name on the right is not so bad:

val loop3: Map[Int, T] = (0 -> 1, 1 -> 2, 2 -> 0)

Unusual? Maybe, but these just landed in C# 12 as Sequence Expressions a few months ago:

// Create an array:
int[] a = [1, 2, 3, 4, 5, 6, 7, 8];

// Create a list:
List<string> b = ["one", "two", "three"];

// Create a span
Span<char> c  = ['a', 'b', 'c', 'd', 'e', 'f', 'h', 'i'];

// Create a jagged 2D array:
int[][] twoD = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

Sequence expressions would also be directionally in line with other experimental Scala 3 features such as Numeric Literals, which also overload the literal syntax to allow various types to be constructed from the same numeric literal syntax via target typing

This is maybe separate/orthogonal to the original proposal, but IMO the current level of “boilerplate” in Scala is “fine”, but nowhere near ideal. Apart from dynamic languages like Python or Javascript that have concise untyped collection literals, we see languages like C# moving towards that level of conciseness while fully statically-typed by making use of target-typing. Given the renewed vigor with which Java has been evolving, I wouldn’t be surprised at all if Java followed suit in a year or two


Circling back to the original proposal, Swift’s usage of leading .foo to refer to pkg.prefix.TargetType.foo is a small but significant improvement over Scala, which traditionally has 3 bad options:

  1. Verbosely use fully-qualified names to refer to things
  2. Verbosely importing things and polluting your local namespace
  3. Avoiding hierarchical namespaces, and putting everything in one huge flat namespace you import stuff.*

The leading .foo solves this entirely: it allows you to be totally concise, keep your local scope clean, while still organizing your code neatly into hierarchical namespaces. And the target-type-driven desugaring is simple, understandable, and unambiguous.

It’s definitely an unusual syntax, and superficially less elegant than un-prefixed foo, but the . has so many upsides in backwards-compat, simplicity of spec, and un-ambiguousness (for humans) that IMO it’s worth it

3 Likes

Swift calls it Implicit Member Expressions

Thanks. No way I’m using “Implicit” for this :sweat_smile:

3 Likes

Shortcut member selection ?

Target member expression

More Devil’s advocate:

Way back we said

val loop3 = Map(0 -> 1, 1 -> 2, 2 -> 0)

was a great example of type inference that saves that unsightly boiler plate from Java. (Edited to remove even more boilerplate.)

Maybe backing out of the “declare all types” guideline (and go back to the “declare types that you have to put some thought into” guideline) is a gentler option than tweaking the language.

1 Like

Sorry for being late to the party, but I encountered this little nuisance myself multiple times as well. So i would really welcome a solution. But, if the starting dot cannot be omitted, please drop the whole idea, for it not only looks kind of ugly, but really is conceptually confusing, especially for beginners.

That said, the two places where spelling out companion object members is most annoying are in the (indirect) constructions and its definition, so i would limit the automatic import to those cases.

Furthermore, these automatic imports should for instance only work if they are allowed explicitly in the definition of the companion object. Maybe we can use the keyword export here (if it does not bite other use). This further reduces compatibility problems, the downside being that it will only work for new cases. The original definition can then be written like:

final case class Shape(geometry: Geometry, color: Color) 

object Shape :
  sealed trait Geometry
  export object Geometry { ... }
  sealed trait Color
  export object Color { ... }

and be used as:

val redCircle = Shape(Circle,Red)

in pattern matching, like before and in related methods:

def myCircle(color: Color): Shape = Shape(Circle,color)

but not in unrelated calls:

case class Model(geometry: Shape.Geometry, color: Shape.Color)  // works, spelled out
val model = Model(Circle,Red) // compiler error, not spelled out, no Shape in sight.
1 Like

Swift users have no beginners? I just understand this argument.

Yes, of course, but i cannot judge if this is conceptually difficult in Swift, maybe not, i don’t know enough about Swift. And probably nobody in this forum is really capable to judge if some new concept is really difficult for beginners, for the lack of beginners. But we know, and i experienced it myself, that the concept of companion objects can be difficult for beginners. And we also know that for example implicits are, for it is invisibility. This is a combination of both, so my fear is that the lonely dot, summoning the companion object before it, might be confusing. Maybe i am wrong, time will tell. And even if so, it still looks ugly.

Am i also curious about your opinion on the rest of my post.

1 Like

I agree; if this feature gets added, I wouldn’t teach it, until maybe much later as an “advanced” concept. The notation is very confusing too. It would lead to a lot of trial / error guessing behavior.

(Companion object by itself is not too bad to explain / teach. A good explanation is “stuff that doesn’t change from instance to instance can go here”. Students generally do OK with the instance concept.)

2 Likes

One important thing about the proposal is that nothing needs to be done in the declaration site to use this feature. Heck, you can even implement this in Scala 3 and have the feature available when using Scala 2 objects. “Ugly” is not a valid enough reason for me to drop this. Is using a $. lead any better?

That would be nice, but is it necessary? Are there a lot of libs out there were this would be an improvement? (Maybe so, its an honest question, i would really not know). But to me, it would be something that i need for myself. In that case there is no problem with changes on the declaration site, which would in turn ensure backward compatibility.

No, not better, both look weird, and from what i read in the posts above, i am not the only one. I would rather write Shape. instead. And if that feels to long, we can aways write import ...{Shape as $} somewhere at the top. So where is the gain then?

To me, the beauty in your proposal is that wherever we use Geometry or Color in there natural habitat (where Shape is present/dominant) they just work without further ado.

1 Like

The proposal outlines the gain and it’s not import aliasing.