Supporting import in extension method blocks

Hmm – that does lead to the reverse question: is there any way we could clarify the difference?

I agree with @alvae’s intuition: this feels syntactically like it should be a scope (with braces or without), and while I intellectually understand the reason why it can’t be, that’s still a surprise. I suspect that this is going to be an ongoing FAQ and small pain point for folks. So I wonder if there’s anything that could be done to make this feel less like a scope?

2 Likes

It seems to me that this convenience syntax is the cause of the confusion.

I could see an argument for supporting import here as another “convenience”, maybe implemented with the semantics described above.

But this seems like a slippery slope. So coming back to this …

I say yes, we can deprecate the convenience syntax and require the extension prefix before every such method. It also doubles down on the idea that extension methods are desugared into curried defs (and can indeed be invoked like that).

2 Likes

Repeat the import in each def would be a leaky abstraction, since then you can’t import a type in a parameter list.

object O:
  type M
extension (x: A) 
  import O.*
  def f(x: M) = ... // does not work
1 Like

It seems like something like this would match the expectation created by the creation of the block (with delimiters like braces/colon or without, it still looks like a block):

Source:

case class PrexistingPersonClass(
  name: PrexistingPersonClass.Name, 
  age: Int
)
object PrexistingPersonClass:
  type Name = String

extension (p: PrexistingPersonClass)
  import p.*
  import PrexistingPersonClass.*

  def isNamed(x: Name): Boolean = name.equalsIgnoreCase(x)

Reality:

case class PrexistingPersonClass(
  name: PrexistingPersonClass.Name, 
  age: Int
)

object PrexistingPersonClassExtensions$1: 
    import PrexistingPersonClass.*

    extension (p: PrexistingPersonClass) def isNamed(x: Name): Boolean = { 
      import p.* 
      name.equalsIgnoreCase(x)
    }

export PrexistingPersonClassExtensions$1.*
1 Like

Maybe the solution is education through better error reporting.

For example, the compiler could detect the presence of an import after the prefix and point the user towards moving it into the def.

2 Likes

A previous proposal was

extension (import x: X) def f = x.y

Theoretically we could allow some other (than { … } or <indent> … <unindent>) syntax for non-scope-creating-antiboilerplate-convenience. If we do, we should generalize it to other “directives” (not only to extension). For example:

private (
  def …
  def …
)
extension (x: A) (
  def …
  def …
)

or

private [[
  def …
  def …
]]
extension (x: A) [[
  def …
  def …
]]

But it’s probably not worth it.

I appreciate the arguments given above for why we can’t readily support import at the top of an extension “block” as I’d initially suggested.

However, I do think there may be a case for an import syntax specifically targeting the members of the extension parameter, eg:

extension (import x: X) 
  def f
  def g

or alternately:

extension import (x: X) 
  def f
  def g

The effect would be for compiler to insert import x.{*, given} at the top each extension method.

I first came to Scala, and still prefer it today, because the language offers industry-leading capabilities for factoring out shared structure in programs. The ability to extract common structure in code is what underlies Scala’s conciseness, productivity, and “convenience”, in my view.

Here we have an emerging example of common structure in a relatively young feature, extension methods. In object-oriented languages, references to an instance’s this pointer are considered so common that they are typically resolved implicitly. Extension methods associated with a parameter likely need to make similarly frequent reference to members of that parameter.

“Good” code typically has many small methods, and so the overhead of an import statement at top of each method is significant; it was personally working on an extension with 10 methods, each 1-2 lines long, that first drove me to post this topic.

In terms of precedents, Kotlin’s extension methods implicitly import the members of the extended instance:

class Circle (val radius: Double) {
}
fun main() {
    fun Circle.perimeter(): Double {
        return 2*Math.PI*radius;
    }
}

While in Swift and C# the extended parameter must be explicitly referenced.

2 Likes

It’s only a partial solution to Nicolas Stucki’s sticking point:

as I forgot to add.

Note that the objection,

is not germane in so far as an import is an import context. It might be hairy to explain what a collective thing is and why an import context closes when it ends.

To collect is to glean, as at harvest, so a collective is for the purpose of lesen, or reading, a convenience for reading. When the harvest is over, it’s natural to close what was opened. Another agrarian pun for collective is колхоз. Imports are for the collective, which does not produce all the grains required for sustenance, and what is produced by the collective is for distribution.

Am I the only one who loves the current syntax? It’s really, really awesome. Maybe I’m just a naive idiot.

As long as people learn the fact “extension does not create a new scope” I think it’s really nice.

I always think of it from a teaching perspective: I’d tell students “look there is no colon here, so no scope”. But then again, I enjoy life on the sunny side, in the strictly-Scala3-only world. So I don’t have to worry about braces. (I do love Curly Brace, she’s a killer robot from the future.)

Otherwise I’d be OK with Arman’s suggestion of requiring extension keyword in front of every method. That’s also good.

I think extension should not create a new scope, regardless of whether it could or could not. That will lead to a lot more confusion. Scopes are more confusing and mentally tasking than other things in general. Students would ask: “where should I import? Up above, or inside here, or somewhere else?” There is already some confusion about whether extensions should be at top-level, or inside a class or (companion) object, etc.

You know people are gonna do some crazy stuff with that kind of mechanism. It then starts going back towards the implicit class stuff of Scala 2. As I understand, extension was created to limit that power and clarify / emphasize the intent / use case over the mechanism.

We should go in the opposite direction. I support Arman’s suggestion (but would love to keep the current totally awesome syntax if I can).

3 Likes

Thanks, I love how Curly Brace and Quote can wield weapons “without any additional training.”

If we could figure that out for Scala, it would reduce the burden on documentation.

As a first step, the docs could indicate which features can be deployed “without special training,” and also which features that, once deployed, ensure the app can “withstand incredible amounts of damage.”

Personally, I’m always asking “where should I import?” If a local import gets pushed to the top of the file, it means not only that this file has a dependency, but the whole file has a dependency.

I hope the people learn that extension does not create a scope, but import creates a context. There are other situations where you go, oh, import creates a context? so the knowledge generalizes.

The people deserve knowledge because knowledge is power!

1 Like

But that would cause confusion as well. There are many places where there is no colon, but there is a new scope. In fact, every place where there are braces or indentation except for an extension block.

2 Likes

Ah you’re right. I guess it’s impossible to find a consistent “rule” to teach these things, everything has to be explained in a “context-sensitive” manner. (At least I don’t have to deal with braces.)

Couldn’t we desugar that to:

object O:
  type M
extension (x: A) 
  def f(x: O.M) =
    import O.*
    ...

Or maybe crazier, allow import parameter clauses:

def f(x: A)(import O.*)(x: M)

This could be internal-only of course

Overall, I’m really not a fan of this “It’s not a scope because there’s no colon (:)” argument

To me Scala is the language of “looking right”, things behave as they look, and extensions look like they create a scope, with or without colon, and that’s why many people want to use a colon there !

And we can create scopes without braces or colons:

x match
  case a =>
    import whatever.I.want
    "This is a scope !"

For me colon is just a clunky way of saying “I’m going to start an indented section here”
(And I am fairly confident we could completely do “colon elision”, in a way not so different from how we did semi-colon elision)

3 Likes

Actually given ... with has no colon and provides scope.

1 Like

Lots of keywords create a scope, e.g. then, else, do, yield, the list goes on. (They are listed in the spec).

I think this convenience is what creates an illusion of a new scope being opened since the extension parameters are then visible everywhere below.

Could there be another version of scope we could use here without any ‘state’ based entries (val / var)? It does seem natural syntactically to allow imports / exports / defs / type aliases here in this stateless scope, even though I get that its very far from whats happening under the hood.

1 Like

My main use case for extensions is to add methods to collection wrapper classes such as

case class Tracks(tracks: Vector[Track]):
...

extension(tracks: Vector[Track])
...

Unless there is some technical reason not to do so, I say just make the import automatic (as in Kotlin?) and be done with it. That would be consistent with adding a method within the class constructor itself if you had access to the source code, so I don’t see how that could be a problem.

Why should I have to write

def timeRange = ScalarRange(tracks.head.time, tracks.last.time)

when I can just write

def timeRange = ScalarRange(head.time, last.time)

An explicit import declaration as others have suggested would be the next best thing, but I don’t see why it should even be needed since it is not needed in the class constructor itself.

After thinking about it a bit more, it occurred to me that we could take it a step further and not even require a name for the extension, as in

extension(Vector[Track])

as opposed to

extension(tracks: Vector[Track])

The name could even be optional, depending on user preference. If no name is given, then the current instance would be referred to as this, and the method would be identical to what it would be if it were inside the class constructor. That would mean that methods could be moved from the class constructor to an extension and back with no changes whatsoever. I like that idea. (I hope I am not missing something obvious here.)