riccardo.cardin
riccardo.cardin

Reputation: 8353

Can't execute an extension method of a context function in Scala 3

I defined an extension method on a context function in Scala 3:

object Scope {
  extension [E, A](a: List[E] ?=> A) def extFoo: A = foo(a)
  
  private def foo[E, A](a: List[E] ?=> A) = {
    given s: List[E] = List.empty
    println(a)
    a
  }
}

However, when I try to use it, the compiler complains. The following @main:

@main def main(): Unit = {
  val i: List[String] ?=> Int = 1
  import Scope.extFoo
  i.extFoo
}

generates this error:

No given instance of type List[String] was found for parameter of (List[String]) ?=> Int
  i.extFoo

Everything works fine if I call the extension method with the alternative syntax, extFoo(i).

Is it the expected behavior?

Upvotes: 1

Views: 190

Answers (2)

Łukasz Biały
Łukasz Biały

Reputation: 470

While Dmytro's answer does allow to apply the extension method it does wrap our i lambda into another lambda. Here's an example piece of code:

λ cat test.scala
extension (x: String ?=> String) def extFoo: String = foo(x)

def foo(f: String ?=> String): String =
  given inside: String = "inside"
  val r = f
  println(s"in foo: ${r}")
  r

val x: String ?=> String =
  println(s"in x: ${summon[String]}")
  summon[String]

@main def main =
  val x2: String ?=> String = ((_: String) ?=> x).extFoo

  given outside: String = "outside"
  x2

when executed with scala-cli run test.scala this yields:

λ scala-cli run .
Compiling project (Scala 3.4.1, JVM (17))
Compiled project (Scala 3.4.1, JVM (17))
in x: inside
in foo: inside

which might be a small surprise for some I guess. Now scala-cli compile --server=false -O -Xprint:genBCode test.scala shows that we have more wrapping than we'd ideally want to:

[[syntax trees at end of                  genBCode]] // /Users/lbialy/Projects/foss/tmp/extensions-on-ctx-funs/test.scala
package <empty> {
  @SourceFile("test.scala") final module class test$package extends Object {
    def <init>(): Unit =
      {
        super()
        x =
          {
            closure(this.$init$$$anonfun$1)
          }
        ()
      }
    private def writeReplace(): Object =
      new scala.runtime.ModuleSerializationProxy(classOf[test$package])
    extension (x: Function1) def extFoo: String = foo(x)
    def foo(f: Function1): String =
      {
        lazy var inside$lzy1: scala.runtime.LazyRef =
          new scala.runtime.LazyRef()
        val r: String = f.apply(this.inside$1(inside$lzy1)).asInstanceOf[String]
        println("in foo: ".+(r))
        r:String
      }
    private <static> val x: Function1
    def x(): Function1 = x
    @main def main(): String =
      {
        lazy var outside$lzy1: scala.runtime.LazyRef =
          new scala.runtime.LazyRef()
        val x2: Function1 =
          {
            closure(<empty>.this.$anonfun$1)
          }
        x2.apply(this.outside$1(outside$lzy1)).asInstanceOf[String]
      }
    private final def $init$$$anonfun$1(using contextual$2: String): String =
      {
        println("in x: ".+(contextual$2))
        contextual$2
      }
    private final def inside$lzyINIT1$1(inside$lzy1$1: scala.runtime.LazyRef):
      String =
      inside$lzy1$1.synchronized[String](
        (if inside$lzy1$1.initialized() then inside$lzy1$1.value() else
          inside$lzy1$1.initialize("inside")).asInstanceOf[String]
      )
    private final lazy given def inside$1(inside$lzy1$2: scala.runtime.LazyRef)
      : String =
      (if inside$lzy1$2.initialized() then inside$lzy1$2.value() else
        this.inside$lzyINIT1$1(inside$lzy1$2)).asInstanceOf[String]
    private final <static> def $anonfun$1$$anonfun$1(using _$1: String): String
       = x().apply(_$1).asInstanceOf[String]
    private final <static> def $anonfun$1(using contextual$3: String): String =
      extFoo(
        {
          closure(this.$anonfun$1$$anonfun$1)
        }
      )
    private final def outside$lzyINIT1$1(outside$lzy1$1: scala.runtime.LazyRef)
      : String =
      outside$lzy1$1.synchronized[String](
        (if outside$lzy1$1.initialized() then outside$lzy1$1.value() else
          outside$lzy1$1.initialize("outside")).asInstanceOf[String]
      )
    private final lazy given def outside$1(outside$lzy1$2: scala.runtime.LazyRef
      ): String =
      (if outside$lzy1$2.initialized() then outside$lzy1$2.value() else
        this.outside$lzyINIT1$1(outside$lzy1$2)).asInstanceOf[String]
  }
  @SourceFile("test.scala") final class main extends Object {
    def <init>(): Unit =
      {
        super()
        ()
      }
    <static> def main(args: String[]): Unit =
      try
        {
          test$package.main()
          ()
        }
       catch
        {
          case
            error @ _:scala.util.CommandLineParser.CommandLineParser$ParseError
             => scala.util.CommandLineParser.showError(error)
        }
  }
  final lazy module val test$package: test$package = new test$package()
}

x2 becomes the closure of $anonfun$1 which in turn wraps a closure of $anonfun$1$$anonfun$1 in extFoo call. If we modify the code to direct application of extension function as a function:

@main def main =
  val x2 = extFoo(x) // should be: x.extFoo
  given outside: String = "outside"
  x2

the output from compiler looks like what we'd want:

    @main def main(): String =
      {
        lazy var outside$lzy1: scala.runtime.LazyRef =
          new scala.runtime.LazyRef()
        val x2: String =
          extFoo(
            {
              closure(<empty>.this.$anonfun$1)
            }
          )
        x2:String
      }

Now I guess the correct question should be why can't we refer to context lambdas without forcing application in any way. There's also an interesting thing happening when you actually do

given String = "outside"
x.extFoo

this compiles but now the output is:

in x: outside
in foo: outside

why? well, because:

"a String is fine too".extFoo

this works and prints:

in foo: a String is fine too

So, a String is a valid value for a function that expects String ?=> String. Why? Because the function doesn't have to use the context it's provided and therefore a String instance is also a valid body of said function. This does generate a method when viewed from -Xprint:genBCode level:

    private final <static> def main$$anonfun$2(using contextual$5: String):
      String = "a String is fine too"

    // in main()
        extFoo(
          {
            closure(<empty>.this.main$$anonfun$2)
          }
        )

Hope my answer helps a bit to explain why i.extFoo compiles at least - it just applies the extension to the result of the context lambda. extFoo(i) on the other hand works because it takes a Function1 and that's what i is. We can't refer to it without wrapping it in another context lambda though :(

Upvotes: 4

Dmytro Mitin
Dmytro Mitin

Reputation: 51703

In i.extFoo what doesn't compile is i itself. i looks for an implicit List[String] in the scope and doesn't find such implicit.

This is consistent with docs

given ec: ExecutionContext = ...

def f(x: Int): ExecutionContext ?=> Int = ...

f(2)(using ec)   // explicit argument
f(2)             // argument is inferred

https://docs.scala-lang.org/scala3/reference/contextual/context-functions.html

.extFoo is tried to be applied to the result of such application, not to the original implicit function.

....extFoo can be resolved or not depending on existence of extension method in the scope but .extFoo can't fix compilation of ....

https://scala-lang.org/files/archive/spec/3.4/07-implicits.html#views

If you mean that .extFoo should be applied to the original implicit function then you can specify this with implicit lambda

val i: List[String] ?=> Int = 1
import Scope.extFoo
((_: List[String]) ?=> i).extFoo // compiles

Upvotes: 5

Related Questions