Search code examples
scalainlineimplicitscala-3given

"Deferred inline method `foo` in trait `Foo` cannot be invoked": Pairs


I was just experimenting with the behavior of givens and inline in Scala 3.2.2, and ran into the following example:

trait Max[X]:
  inline def max(a: X, b: X): X
  
inline given maxForDoubles: Max[Double] with
  inline def max(a: Double, b: Double) = if a < b then b else a

inline given maxForPairs[A, B](using mA: Max[A], mB: Max[B]): Max[(A, B)] with
  inline def max(x: (A, B), y: (A, B)) =
    (mA.max(x._1, y._1), mB.max(x._2, y._2))

@main def entryPoint(): Unit = {
  println(summon[Max[(Double, Double)]].max((10.0, 3.0), (20.0, -7.0)))
}

Instead of just printing the pair (20, 3), it fails the compilation with the following error:

12 |  println(summon[Max[(Double, Double)]].max((10.0, 3.0), (20.0, -7.0)))
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |          Deferred inline method max in trait Max cannot be invoked
   |----------------------------------------------------------------------------
   |Inline stack trace
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |This location contains code that was inlined from buggy-example.scala:9
 9 |    (mA.max(x._1, y._1), mB.max(x._2, y._2))
   |     ^^^^^^
    ----------------------------------------------------------------------------

It seems that it should know everything statically. Any clues why it's failing?


Solution

  • If you tried to use alias givens rather than given instances (with-syntax)

    inline given maxForDoubles: Max[Double] = new Max[Double]:
      inline override def max(a: Double, b: Double): Double = if a < b then b else a
    
    inline given maxForPairs[A, B](using mA: Max[A], mB: Max[B]): Max[(A, B)] = new Max[(A, B)]:
      inline override def max(x: (A, B), y: (A, B)): (A, B) = (mA.max(x._1, y._1), mB.max(x._2, y._2))
    

    this wouldn't work because of

    Implementation restriction: nested inline methods are not supported
    

    But with with-syntax nested inline are allowed because

    inline given maxForDoubles: Max[Double] with
      inline override def max(a: Double, b: Double): Double = if a < b then b else a
    
    inline given maxForPairs[A, B](using mA: Max[A], mB: Max[B]): Max[(A, B)] with
      inline override def max(x: (A, B), y: (A, B)): (A, B) = (mA.max(x._1, y._1), mB.max(x._2, y._2))
    

    is actually

    class maxForDoubles extends Max[Double]:
      inline override def max(a: Double, b: Double): Double = if a < b then b else a
    
    inline given maxForDoubles: maxForDoubles = new maxForDoubles
    
    class maxForPairs[A, B](using mA: Max[A], mB: Max[B]) extends Max[(A, B)]:
      inline override def max(x: (A, B), y: (A, B)): (A, B) = (mA.max(x._1, y._1), mB.max(x._2, y._2))
    
    inline given maxForPairs[A, B](using mA: Max[A], mB: Max[B]): maxForPairs[A, B] = new maxForPairs[A, B]
    

    Indeed, with -Xprint:pickleQuotes option (or -Xprint:typer) the with-syntax produces

    //    given class maxForDoubles() extends Object(), App.Max[Double] {
    //      override inline def max(a: Double, b: Double): Double = 
    //        (if a.<(b) then b else a):Double
    //    }
    //    final inline given def maxForDoubles: App.maxForDoubles = 
    //      new App.maxForDoubles():App.maxForDoubles
    //    given class maxForPairs[A >: Nothing <: Any, B >: Nothing <: Any](using 
    //      mA: App.Max[A]
    //    , mB: App.Max[B]) extends Object(), App.Max[
    //      Tuple2[maxForPairs.this.A, maxForPairs.this.B]
    //    ] {
    //      A
    //      B
    //      protected given val mA: App.Max[A]
    //      protected given val mB: App.Max[B]
    //      override inline def max(x: Tuple2[maxForPairs.this.A, maxForPairs.this.B]
    //        , 
    //      y: Tuple2[maxForPairs.this.A, maxForPairs.this.B]): (A, B) = 
    //        Tuple2.apply[A, B](this.App$maxForPairs$$inline$mA.max(x._1, y._1), 
    //          this.App$maxForPairs$$inline$mB.max(x._2, y._2)
    //        ):(A, B)
    //      def App$maxForPairs$$inline$mA: App.Max[A] = maxForPairs.this.mA
    //      def App$maxForPairs$$inline$mB: App.Max[B] = maxForPairs.this.mB
    //    }
    //    final inline given def maxForPairs[A >: Nothing <: Any, B >: Nothing <: Any]
    //      (
    //    using mA: App.Max[A], mB: App.Max[B]): App.maxForPairs[A, B] = 
    //      new App.maxForPairs[A, B](using mA, mB)():App.maxForPairs[A, B]
    

    Let's temporarily simplify the with-syntax removing using and maxForDoubles

    class maxForPairs[A, B]/*(using mA: Max[A], mB: Max[B])*/ extends Max[(A, B)]:
      inline override def max(x: (A, B), y: (A, B)): (A, B) = ??? // (mA.max(x._1, y._1), mB.max(x._2, y._2))
    
    // inline given maxForPairs[A, B]/*(using mA: Max[A], mB: Max[B])*/: maxForPairs[A, B] = new maxForPairs[A, B]
    

    Then the difference between

    val inst: maxForPairs[Double, Double] = new maxForPairs[Double, Double]
    inst.max((10.0, 3.0), (20.0, -7.0)) // compiles
    

    and

    val inst: Max[(Double, Double)] = new maxForPairs[Double, Double]
    inst.max((10.0, 3.0), (20.0, -7.0)) // doesn't compile: Deferred inline method max in trait Max cannot be invoked
    

    is understandable because there is a rule

    1. Inline methods can also be abstract. An abstract inline method can be implemented only by other inline methods. It cannot be invoked directly:

      abstract class A:
        inline def f: Int
      
      object B extends A:
        inline def f: Int = 22
      
      B.f         // OK
      val a: A = B
      a.f         // error: cannot inline f in A.
      

    https://docs.scala-lang.org/scala3/reference/metaprogramming/inline.html#rules-for-overriding

    And max is exactly an abstract inline method in the trait Max.

    summon[Max[(Double, Double)]] returns the precise type of implicit: maxForPairs[Double, Double] <: Max[(Double, Double)] (as shapeless.the in Scala 2), not just Max[(Double, Double)] (as implicitly in Scala 2).

    But if we restore using and maxForDoubles then for some reason this confuses inlines: both options for val inst (: maxForPairs[Double, Double] and : Max[(Double, Double)]) produce Deferred inline method max in trait Max cannot be invoked.

    This seems to be a bug or underspecified feature. Try to open a ticket at https://github.com/lampepfl/dotty/issues

    By the way, sometimes with-syntax behaves weirdly in comparison with alias givens (even without inlines): https://github.com/lampepfl/dotty/issues/8882