Relative scoping for hierarchical ADT arguments

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)

1 Like

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:

  1. .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

  2. 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)

  3. 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

    1. 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.
    2. 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
    3. 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)
  4. 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

2 Likes

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 ..

1 Like

I gave a solid use-case above where this limitation is too restrictive.

Which one ?
I could not recall an example where expected type alone was not enough

1 Like

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.

trait Foo
object Foo{
  object stuff{
    def nestedFactory(): Foo
  }
}

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)
        }

What about marking at the definition site which parameter allows relative scoping, as in:

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

Or even do this for target typing as such:

final case class Shape(@tagetType geometry : Shape.Geometry, @targetType color : Shape.Color)

It would allow to disambiguate in case of overloaded variants.

Wouldn’t it work tho ?

options.CompilerOptions.ParserLogLevel has a companion object options.CompilerOptions.ParserLogLevel with a member INFO of that type ?
(since ParserLogLevel <: LogLevel)

Not with the proposed restriction. LogLevel.INFO is not the type of ParserLogLevel. It can only be that through implicit or explicit conversion.

But it is of type LogLevel, no ?

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.

So I think this would defeat the point.

4 Likes

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 infix discussion. 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.