I don’t think this is a good analogy, (hence why it seems counterintuitive) I think a better analogy would be " 's " in english: Mike’s red ↔ Mike.Red, in that way, a prefix period is like using “one’s”, “someone’s” or “his”: his red ↔ .Red
In that light, it makes a lot more sense, and feels more intuitive
Maybe we could add something instead of removing the period, to be more in line with “his”, for example *.Red, ?.Red or ...Red
(But regardless, I am not sure either analogy is useful in deciding if the period is a good choice)
I don’t think “dismissed” is the right word here. They were discussed in detail and their tradeoffs enumerated repeatedly. I can repeat some of them again below:
.foo syntax is ambiguous in a small number of cases due to method chaining over multiple lines. But un-prefixed foo syntax is ambiguous all the time with any variable that you may have in scope with the same name
Un-prefixed foo can be disambiguated by resolution fallback rules in the typer, which is less obvious both to machines and to humans than .foo syntax which can be disambiguated by precedence rules in the parser. Having to run a full typechecking name resolution to figure out where the feature is taking effect is much more involved for machines and humans than simply parsing the code in question (even if you then can only expand the .foo to its fully qualified path during/after typechecking)
Un-prefixed foo can come in two variants: either it is opt-in with a flag (per-method param, or per-type) to enable, or it applies universally to every definition side
If it is opt-int, that means it cannot be used on existing libraries unless retrofitted. This is less than ideal, since there’s a ton of existing code that could benefit right away. e.g. all code using enums, sealed traits with their cases in the companion, types with factory methods in their companion, etc.
if it is on by default for everyone, then it probably brings into scope way too much stuff into every expression that has a target type. “everything in the companion object Foo” is a lot of stuff to bring into scope every time a type Foo is expected
Alternately, it could only apply to special members of the companion, e.g. only the constructor. This limits the scope pollution, but limits the usefulness v.s. the original implementation in Swift where calling factory methods of the target type on its companion was a major use case (including those taking parameters)
IIRC @odersky himself has repeatedly rejected the idea of scope injection, or bringing additional identifiers into scope in a user-configurable way. I can’t google up any examples at the moment, but I quite clearly remember that being the case, going back long before this particular proposal. Therefore it is not surprising that an approach that involves bringing new identifiers into scope in a user-configurable way is deemed unlikely to get support
It’s not so much dismissal as a study of pros and cons. I don’t dispute that .foo has an “ick” factor and looks very unusual. But Swift seems to demonstrate that the “ick” factor is a non-issue for a wide base of not-necessarily-sophisticated users (iOS app developers), which to me indicates that the Scala community should be able to get used to it as well.
If we decide to go with an un-prefixed foo, I would be fine with that too. It’s just the arguments in favor of having a prefixed .foo do seem very reasonable
If it is on by default for everyone, then it probably brings into scope way too much stuff into every expression that has a target type. “everything in the companion object Foo ” is a lot of stuff to bring into scope every time a type Foo is expected
I think after an initial experimental phase it should be on always. or we should drop the idea. I am against adding additional mode switches. About the concern of bringing into scope “way too much stuff”, I was imagining to restrict it to members that actually return a value of the target type. E.g. if the expected type is Color then Red could be referenced unqualified but values could not.
It’s true that this is a form of scope injection and I am generally not a fan of that. So I am still sitting on the fence here. However, if we want to have this form of target scoping, then would prefer unqualified over prefix ..
I think this sounds like a reasonable restriction. That should significantly cut down on the scope pollution, while still bringing in everything that would be useful. And if we make it a fallback scope only looked up if the existing name resolution falls through, it would be 100% source and binary compatible
Restricting it to only members that return a value of the type does rule out things like factory methods inside nested objects, e.g.
But maybe that’s an uncommon enough use case it’s OK.
One thing I’d like to call out, that maybe hasn’t been said explicitly here, is that this “relative scoping” should work for pattern matching as well. e.g. This is the case in Java enums and switch statements, where un-qualified names are required:
enum Level {
LOW,
MEDIUM,
HIGH
}
class HelloWorld {
public static void main(String[] args) {
Level myVar = Level.MEDIUM;
switch(myVar){
case LOW: System.out.println("low"); break;
case MEDIUM: System.out.println("medium!"); break;
case HIGH: System.out.println("high!!!"); break;
}
}
}
And in Swift, where qualified names are allowed, but dot-prefixed shorthand is normally used:
switch state {
case nil:
removeLoadingSpinner()
removeErrorView()
renderContent()
case .loading?:
removeErrorView()
showLoadingSpinner()
case .failed(let error)?:
removeLoadingSpinner()
showErrorView(for: error)
}
options.CompilerOptions.ParserLogLevel has a companion object options.CompilerOptions.ParserLogLevel with a member INFO of that type ?
(since ParserLogLevel <: LogLevel)
Definition site differences aren’t apparent when reading code. Having magic be fickle in response to the whimsy of the library designer just means you can’t rely on it, so your code style standard for readability should be: always use the fully qualified name.
True, but i don’t see the problem here. The way things are defined at the definition site are always relevant. See for example the whole infixdiscussion. I agree that a solution that works without being dependant on the definition site is attractive, but it depends on the price that it comes with. There is always some catch, so choosing what is “better” is a matter of opinion.
Reading through all the posts in this thread, I see a lot of resistance for a leading dot. Yes, other languages may have this too, but these are other languages, so the language construct may induce a different feeling. For Scala it does not feel good. Allowing changes at definition site it just one way to solve this, as I also tried to put forward. There are others as well.
Now i am just an other Scala user, not even an expert, so what gives. In the end, the number of people that use our language will show if we made the right choices overall.
Notably, Dart just just approved and shipped dot-delimited expressions in 3.10. So that makes 2-3 languages with this feature (Swift, Dart, C# under discussion)
Yes, I think some kind of prefix is mandatory and reduces surprise.
I also think we should only enable this feature in a named argument position. This would help remove some of the edge-cases discussed with a dot prefix and guarantees that context is always provides.
enum BigLampCondition:
case Off, Movement, On
enum SmallLampCondition:
case Off, On
def setMyHome(bigLamp: BigLampCondition, smallLamp: SmallLampCondition): Unit = ???
//this is well understood
setMyHome(bigLamp = .On, smallLamp = .On)
setMyHome(BigLampCondition.On, SmallLampCondition.On)
//this is not
setMyHome(.On, .On)
I think @mberndt’s idea of using a # prefix to represent the companion object of the target type is definitely worth considering.
Translating the headline usage example from the C# proposal to Scala:
type.getMethod("Name", #Public | #Instance | #DeclaredOnly); // BindingFlags.Public | ...
control.foreColor = #Red; // Color.Red
entity.invoiceDate = #Today; // DateTime.Today
readJsonDocument(#parse(stream)); // JsonDocument.parse
// static members on Option[int]
val option: Option[Int] = if condition then #none else #some(42);
// nested types in companion object
val result: CustomResult = if condition then #Success(42) else #Error("message");
result match {
case #Success(foo) => foo
case #Error(msg) => defaultVal
};
They can also serve as collection literal syntax and lightweight case-class constructor syntax:
// collection literals
val arr: Array[Int] = #(1, 2, 3)
// map literals
val map: Map[Int, String] = #(1 -> "one", 2 -> "two", 3 -> "three")
// case class literals
case class Point(x: Int, y: Int)
val lotsOfPots: Seq[Point = #(
#(x = 1, y = 2),
#(x = 3, y = 4),
#(x = 5, y = 6),
#(x = 7, y = 8),
)
# has never been used much in Scala, and is probably used even less than before with the overhaul of type-level programming in Scala 3 meaning you don’t need type-projections and aux-pattern much anymore
It’s as short as .
It does not exist in term-position at all, except for custom symbolic operators which are pretty niche and used less than they used to be
It isn’t visually mistakable for a type param list like [] is, and isn’t syntactically overlapping with current Scala syntax unlike proposals to use naked control.foreColor = Red or (1, 2, 3) as collection literals or (x = 1, y = 2) as case class literals
It has a single rule: # is like _, except instead of substituting a function parameter/argument into the _ based on the parameter of the expected function type of the surrounding expression, it substitutes the # using the companion object of the expected type. The scope, how it interacts with parentheses, etc. can all follow how _ already works today which everyone is already used to
The #Public or #(...) syntax is definitely a bit unusual, isn’t super pretty, and isn’t as “standard” as [] would be for collection literals. But it does seem nice that for the price of one character, we are able to satisfy the need of 3-4 different language features with a single consistent semantic, and with zero ambiguity (visual or technical) with other parts of the language.
If implemented, I expect # would see widespread use throughout Scala codebases similar to how _ is used today