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.
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
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
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:
- Verbosely use fully-qualified names to refer to things
- Verbosely importing things and polluting your local namespace
- 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
Swift calls it Implicit Member Expressions
Thanks. No way Iâm using âImplicitâ for this
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.
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.
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.
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.)
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.
The proposal outlines the gain and itâs not import aliasing.