Will it become possible to make "contextualized" scope injection using only import-s and extension methods and implicit functions?

precondition: to make this approach fully available `import` should work well with extension methods, so that it should be true that `obj.extensionMethod()` <==> `{import obj._; extensionMethods()}` (here Kotlin could be considered as good reference implementation)

General idea, that if we have some code like obj.someRegularMember() or obj.someExtensionStuff() we should have some construction to “eliminate” that obj. prefix and have possibility to “embed” obj into scope that way, that obj.someRegularMember() and obj.someExtensionStuff() both become available just like someRegularMember() and someExtensionStuff()
In Kotlin this kind of importing could be done easily by with / apply / run constructions (inline ext. methods).
In Scala, naturally, import obj._ could serve for that needs, but unfortunately in current Scala import obj._ does not work well with extension methods (meaning obj.extensionMethods() case)

Meanwhile Kotlin constructions works as expected in that case as well (obj.extensionMethod() <==> {import obj._; extensionMethods()} is true for appropriate Kotlin’s constructions)

    // this is valid Kotlin code - all assertions are passed
    fun testImportsExtMethodFromTarget1(): Unit {
        data class Foo(val payload: Any)
        // define local extension function/method
        fun Foo.bar(): String = "bar($this)"

        val extensionTarget = Foo("abc")
        // when we "importing" entire content of `extensionTarget` into scope, applicable ext-methods will be "imported"
        // as well (so that `extensionTarget.` prefix could be removed not only for regular members access, but for extensions as well)
        with (extensionTarget) {
            // identical to Scala( `import extensionTarget._` )
            assert(bar() == extensionTarget.bar())
            assert(bar() == "bar(Foo(payload=abc))")
        }
    }

Similar Scala snippet written using newly proposed extension methods may look like this:

    // for now one line of this code cannot be compiled using compiler from 'add-extensions' branch
    object testImportsExtMethodFromTarget1 {
        case class Foo(val payload: Any)
        // define local extension method - currently could not write it as local def nested into other def ...
        def bar(this foo:Foo)(): String = s"bar($foo)"

        {
          val extensionTarget = Foo("abc")
          // this works as expected
          assert(extensionTarget.bar() == "bar(Foo(abc))")

          // try to make `with(extensionTarget){ assert (bar() == extensionTarget.bar()) }`
          {
            import extensionTarget._
            // unfortunately this line even could NOT be compiled ... ; error: "missing argument for parameter foo of method bar: (foo: Foo)(): String"
            assert (bar() == extensionTarget.bar()) // <-- code will be compiled only after removing of this line ...
          }
        }

        def apply() = ()
    }

It could be noted that:
- currently nested extension-def-s are not supported, so test code is hosted in object instead of being simple def
- extensionTarget.bar() <==> {import extensionTarget._ ; bar() } - does not work in current state of add-extensions branch. I know that same issue is present in current Scala - for extension methods implemented via implicit conversions, maybe it is more/or/less acceptable for thous “legacy” extension methods to be not imported by import extensionTarget._ construction, but I think it would be quite valid if import extensionTarget._ imports at least extension methods define by specialized extension-methods constructs.

bigger Kotlin example for case `{ import extensionProvider._ ; import extensionTarget._ ; extensionMethod() }` is described inside

Let’s consider that case when we defined some extension method inside extensionProvider instance, and we have extensionTarget instance on which that extension method could be applied. If we make import extensionProvider._ it will make possible to write extensionTarget.extensionMethod() , next natural step, is to “get rid of extensionTarget. prefix” by making import extensionTarget._ . Unsurprisingly Kotlin’s with / apply / run constructions works as expected and allows us to do that.

    data class ExtensionTarget(val intValue0: Int)
    data class ExtensionProvider(val intValue1: Int) {
        // sum() == extensionTarget.intValue0 + extensionProvider.intValue1
        fun ExtensionTarget.sum() = intValue0 + intValue1
    }

    fun testScopesMixing(): Unit {
        val extTarget = ExtensionTarget(10)
        val extProvider = ExtensionProvider(9)
        // in case when both `extTarget._` and `extProvider._` are imported into scope together `.sum` extension method become
        // available also without any `obj.` prefixes ; and it does not matter in which order `extTarget` and `extProvider` was imported
        with (extTarget) {
            with (extProvider) {
                assert(19 == sum())
            }
        }
        with (extProvider) {
            with (extTarget) {
                assert(19 == sum())
            }
        }
    }
more verbose
    data class ExtensionTarget(val intValue0: Int)
    data class ExtensionProvider(val intValue1: Int) {
        // sum() == extensionTarget.intValue0 + extensionProvider.intValue1
        fun ExtensionTarget.sum() = intValue0 + intValue1
    }

    fun testScopesMixing(): Unit {
        val extTarget = ExtensionTarget(10)
        val extProvider = ExtensionProvider(9)
        // in case when both `extTarget._` and `extProvider._` are imported into scope together `.sum` extension method become
        // available also without any `obj.` prefixes ; and it does not matter in which order `extTarget` and `extProvider` was imported
        with (extTarget) {
            with (extProvider) {
                assert(19 == sum())
            }
        }
        with (extProvider) {
            with (extTarget) {
                assert(19 == sum())
            }
        }

        // more verbose explanations are:
        with (extTarget) l1@{
            // now `extTarget` is "embedded" into scope - all its members available as `this.member` or just `member`
            with (extProvider) l2@{
                // now `extProvider` is "embedded" into scope - all its extension methods become available
                // since `ExtensionTarget` instance is already "embedded" into scope `ExtensionTarget`-extensions become available
                // without any prefix
                assert(19 == sum())
                // other equivalent expressions for `sum()` are ; by the way `this.sum()` is NOT valid but `[email protected]()` is
                assert(extTarget.intValue0 + extProvider.intValue1 == sum())
                assert(extTarget.sum() == sum())
                assert([email protected]() == sum() && extTarget == this@l1)
            }
        }

        with (extProvider) l2@{
            // now `extProvider` is "embedded" into scope - all its extension methods become available
            with (extTarget) l1@{
                // now `extTarget` is "embedded" into scope - all its members available as `this.member` or just `member`
                // before last `with` extension `.sum` was available only aa `extTarget.sum()`, last `with` allows us get rid of
                // `extTarget.` prefix and call `sum()` without any prefix as well
                assert(19 == sum())
                // other equivalent expressions for `sum()` are ; by the way `this.sum()` is valid same as `[email protected]()` is
                assert(extTarget.intValue0 + extProvider.intValue1 == sum())
                assert(extTarget.sum() == sum())
                assert(this.sum() == sum() && extTarget == this && this == this@l1)
            }
        }
    }


Let’s consider following simplified example of Kotlin “Type-Safe Builder
And then try to approximate it with Scala code using newly introduced (proposed) scala extension methods (ref docs)

    data class FooBar(val foo: Int, val bar: Int)

    data class FooBarBuilder(var foo: Int = 0, var bar: Int = 0) {
        fun initFoo(foo: Int) {
            this.foo = foo
        }
        fun initBar(bar: Int) {
            this.bar = bar
        }
        inline fun init(initBlock: FooBarBuilder.() -> Unit): FooBarBuilder {
            // same as `this.initBlock()` - initBlock is extension function
            // but it is also applicable to `this` and that is why could be used without any (`this.`) prefix
            initBlock()
            return this
        }
        fun build(): FooBar = FooBar(foo, bar)
    }

    fun testFooBarBuilder(): Unit {
        val builder = FooBarBuilder()
        builder.init {
            // this block is in fact "extension lambda", so it has it's own `this` instead of sharing same `this`
            // with it's hosting method ; "injected scope" is in fact supplied by caller this way
            assert(this is FooBarBuilder) // this line will be warned since it is known to be `true` even at compile-time
            // same as `this.initFoo(10)`
            initFoo(10)
            // same as `this.initBar(20)`
            initBar(20)
        }
        val fooBar = builder.build()
        assert(fooBar.foo == 10)
        assert(fooBar.bar == 20)
    }

Scala approximation then may look like this:


// this is valid code which was checked to be working under current state of 'add-extensions' branch
object TestMain {

  def main(args: Array[String]): Unit = {
    testFooBarBuilder()
  }

  // "Dummy" target for extension methods - should become a bridge in translation from extension methods to "regular methods"
  object `this` {}

  case class FooBar(val foo: Int, val bar: Int)

  case class FooBarBuilder(var foo: Int = 0, var bar: Int = 0) {
    // this builder methods are synthetically defined as extension methods of (object `this`)
    def initFoo(this __ : `this`.type)(foo: Int): Unit = {
      this.foo = foo
    }
    def initBar(this __ : `this`.type)(bar: Int): Unit = {
      this.bar = bar
    }
    def init(initBlock: implicit FooBarBuilder => Unit): this.type = {
      //implicit val `import this`: FooBarBuilder = this
      initBlock(this)
      this
    }
    def build(): FooBar = FooBar(foo, bar)
  }

  def testFooBarBuilder(): Unit = {
    val builder = FooBarBuilder()
    builder.init {
      // same as `this.initFoo(10)` from Kotlin snippet
      `this`.initFoo(10)
      // same as `this.initBar(20)` from Kotlin snippet
      `this`.initBar(20)
    }
    val fooBar = builder.build()
    assert(fooBar.foo == 10)
    assert(fooBar.bar == 20)
    println(fooBar)
  }

}

Now in second (Scala) example initFoo and initBar was synthetically defined as extension methods for
some “dummy object” (namely object `this`), so inside builder.init block they are available “almost” like in Kotlin, in particular

      // same as `this.initFoo(10)`
      `this`.initFoo(10)
      // same as `this.initBar(20)`
      `this`.initBar(20)

So very last question here: whether we can (some how) get rid of that annoying `this`. prefixes by simply importing “all content (including extensions)” from `this` object ? As far as I can check it, for now answer is No.

But it would be nice if below version of code become valid, and logically accomplish suggested solution

  // this code cannot be compiled in current state of  'add-extensions' branch
  def testFooBarBuilder(): Unit = {
    // make import to get rid of `this`. prefix not only  for regular members access, but for extension methods as well
    import `this`._

    val builder = FooBarBuilder()
    builder.init {
      // same as - `this`.initFoo(10) - supposedly ext. method call should become available without `this`. prefix
      initFoo(10)
      // same as - `this`.initBar(20) - supposedly ext. method call should become available without `this`. prefix
      initBar(20)
    }
    val fooBar = builder.build()
    assert(fooBar.foo == 10)
    assert(fooBar.bar == 20)
    println(fooBar)
  }

By the way, this situation reminds me a lot good old Adobe ActionScript namespaces, (1).
In general it allows marking any members with some user defined “tags” (aka “namespaces”), then in other parts of program that members become visible only after importing/“using” that namespace into scope (in their notation, by writing use namespace some_imported_namespace;)

In this case some “dummy” extension target object may serve exactly as such kind of “namespace”. (If import dummy._ will work as expected - it should make “visible” (as regular methods) all that methods which was synthetically defined as extensions of dummy)

And finally. Of course it could not be considered as the best solution of “contextualized scope injection” problem, but I think it can be treated as some raw idea of “transitive importing” implementation - it could be some way to “land” some members of some instances from implicits scope into regular scope (for example by the means of importing some tag-object which will actually activates that transition, in aforementioned
example that object `this` should serve in that position)

Other (even closer) approximation of aforementioned kotlin’s code may look like this

// this is valid code which was checked to be working under current state of 'add-extensions' branch
object TestMain {
  def main(args: Array[String]): Unit = {
    testFooBarBuilder2()
  }

  // marker interface used as "dummy" target for extension methods
  trait FooBarDslNsActivator

  case class FooBar(val foo: Int, val bar: Int)

  case class FooBarBuilder(var foo: Int = 0, var bar: Int = 0) {
    // this builder methods are theoretically defined as extension methods of (object `this`)
    def initFoo(this __ : FooBarDslNs)(foo: Int): Unit = {
      this.foo = foo
    }
    def initBar(this __ : FooBarDslNs)(bar: Int): Unit = {
      this.bar = bar
    }
    def init(initBlock: implicit FooBarBuilder => Unit): this.type = {
      //implicit val `import this`: FooBarBuilder = this
      initBlock(this)
      this
    }
    def build(): FooBar = FooBar(foo, bar)
  }

  object testFooBarBuilder2 extends FooBarDslNsActivator {
    def apply(): Unit = {
      val builder = FooBarBuilder()
      builder.init {
        // `this.initFoo(10)` from kotlin's example will have exactly the same side effect on builder
        this.initFoo(10)
        // `this.initBar(20)` from kotlin's example will have exactly the same side effect on builder
        this.initBar(20)

        // unfortunately `this.initFoo(10)` !== `initFoo(10)` for extension method `initFoo`
        // in current implementation of extension methods resolving
      }
      val fooBar = builder.build()
      assert(fooBar.foo == 10)
      assert(fooBar.bar == 20)
      println(fooBar)
    }
  }

}
1 Like