softwaremill/quicklens

Slow compilation with multiple .modify calls when using Scala3

adamw opened this issue · 6 comments

adamw commented

Original issue: scala/scala3#15945

Code to reproduce:

case class A(x: Double)
case class B(a1: A = A(1), a2: A = A(2), a3: A = A(3), a4: A = A(4))
case class C(b: B)

import com.softwaremill.quicklens._

@main
def main(): Unit = {
  val c = C(B(A(1)))
  c
    .modify(_.b.a1.x).setTo(0)
    .modify(_.b.a2.x).setTo(0)
    .modify(_.b.a3.x).setTo(0)
    .modify(_.b.a4.x).setTo(0)
}

cc @odersky @OndrejSpanel

Maybe related to #81 ?

adamw commented

@OndrejSpanel I tried compiling the above, but I don't see any performance problems. Though I had to replace 0 with 0.0d so that the types match. Are you using latest quicklens version?

It might be that to reproduce it, we will require a type error.
The following code:

//> using scala "3.nightly"
//> using lib "com.softwaremill.quicklens::quicklens:1.8.10"

case class A(x: Double)
case class B(a1: A = A(1), a2: A = A(2), a3: A = A(3), a4: A = A(4), a5: A = A(5))
case class C(b: B)

import com.softwaremill.quicklens._

@main
def main(): Unit = {
  val c = C(B(A(1)))
  c
    .modify(_.b.a1.x).setTo(0)
    .modify(_.b.a2.x).setTo(0)
    .modify(_.b.a3.x).setTo(0)
    .modify(_.b.a4.x).setTo(0)
    .modify(_.b.a5.x).setTo(0)
}

Takes very long to finish compilation for me.

Also, I don't think it has much to do with #81 or #82, since the compilation time seems the same for com.softwaremill.quicklens::quicklens:1.8.2.

adamw commented

Ah, of course, you need a compile error 🤦

The original compiles in ~3seconds, so I didn't notice anything, but now I added a5 and after 2 minutes I'm still waiting ;)

The original compiles in ~3seconds,

This is extract from a real-life project, which took 10 minutes - with the compilation error, but the code was compiling fine with Scala 2. The extract with 4 modify calls shows a constraint explosion (641 constraints , as shown by -Ydetailed-stats option), yet it compiles quickly enough not to be annoyingly slow.

adamw commented

The problem was that we were using the original source when generating the .copy, which in case of many parameters (for all of which parameters had to be provided) caused an exponential code explosion. Caching the value in an intermediate value seems to solve the problem.

Just for fun, that's the code we generated for 2 (not 5) chained .modify invocations:

def x(): Unit = {
  (
      (x: scala.Function1[scala.AnyVal, scala.AnyVal]) =>
        ((com.softwaremill.quicklens.PathModify.apply[com.softwaremill.quicklens.x.C, scala.AnyVal](
          c,
          (
              (`x₂`: scala.Function1[scala.AnyVal, scala.AnyVal]) =>
                (c.copy(b =
                  c.b.copy(
                    a1 = `x₃`.apply(c.b.a1),
                    c.b.copy$default$2,
                    c.b.copy$default$3,
                    c.b.copy$default$4,
                    c.b.copy$default$5
                  )
                ): com.softwaremill.quicklens.x.C)
          )
        ): com.softwaremill.quicklens.PathModify[com.softwaremill.quicklens.x.C, scala.AnyVal])
          .setTo(0)
          .copy(b =
            (com.softwaremill.quicklens.PathModify.apply[com.softwaremill.quicklens.x.C, scala.AnyVal](
              c,
              (
                  (`x₄`: scala.Function1[scala.AnyVal, scala.AnyVal]) =>
                    (c.copy(b =
                      c.b.copy(
                        a1 = `x₃`.apply(c.b.a1),
                        c.b.copy$default$2,
                        c.b.copy$default$3,
                        c.b.copy$default$4,
                        c.b.copy$default$5
                      )
                    ): com.softwaremill.quicklens.x.C)
              )
            ): com.softwaremill.quicklens.PathModify[com.softwaremill.quicklens.x.C, scala.AnyVal])
              .setTo(0)
              .b
              .copy(
                (com.softwaremill.quicklens.PathModify.apply[com.softwaremill.quicklens.x.C, scala.AnyVal](
                  c,
                  (
                      (`x₅`: scala.Function1[scala.AnyVal, scala.AnyVal]) =>
                        (c.copy(b =
                          c.b.copy(
                            a1 = `x₃`.apply(c.b.a1),
                            c.b.copy$default$2,
                            c.b.copy$default$3,
                            c.b.copy$default$4,
                            c.b.copy$default$5
                          )
                        ): com.softwaremill.quicklens.x.C)
                  )
                ): com.softwaremill.quicklens.PathModify[com.softwaremill.quicklens.x.C, scala.AnyVal])
                  .setTo(0)
                  .b
                  .copy$default$1,
                a2 = x.apply(
                  (com.softwaremill.quicklens.PathModify.apply[com.softwaremill.quicklens.x.C, scala.AnyVal](
                    c,
                    (
                        (`x₆`: scala.Function1[scala.AnyVal, scala.AnyVal]) =>
                          (c.copy(b =
                            c.b.copy(
                              a1 = `x₃`.apply(c.b.a1),
                              c.b.copy$default$2,
                              c.b.copy$default$3,
                              c.b.copy$default$4,
                              c.b.copy$default$5
                            )
                          ): com.softwaremill.quicklens.x.C)
                    )
                  ): com.softwaremill.quicklens.PathModify[com.softwaremill.quicklens.x.C, scala.AnyVal]).setTo(0).b.a2
                ),
                (com.softwaremill.quicklens.PathModify.apply[com.softwaremill.quicklens.x.C, scala.AnyVal](
                  c,
                  (
                      (`x₇`: scala.Function1[scala.AnyVal, scala.AnyVal]) =>
                        (c.copy(b =
                          c.b.copy(
                            a1 = `x₃`.apply(c.b.a1),
                            c.b.copy$default$2,
                            c.b.copy$default$3,
                            c.b.copy$default$4,
                            c.b.copy$default$5
                          )
                        ): com.softwaremill.quicklens.x.C)
                  )
                ): com.softwaremill.quicklens.PathModify[com.softwaremill.quicklens.x.C, scala.AnyVal])
                  .setTo(0)
                  .b
                  .copy$default$3,
                (com.softwaremill.quicklens.PathModify.apply[com.softwaremill.quicklens.x.C, scala.AnyVal](
                  c,
                  (
                      (`x₈`: scala.Function1[scala.AnyVal, scala.AnyVal]) =>
                        (c.copy(b =
                          c.b.copy(
                            a1 = `x₃`.apply(c.b.a1),
                            c.b.copy$default$2,
                            c.b.copy$default$3,
                            c.b.copy$default$4,
                            c.b.copy$default$5
                          )
                        ): com.softwaremill.quicklens.x.C)
                  )
                ): com.softwaremill.quicklens.PathModify[com.softwaremill.quicklens.x.C, scala.AnyVal])
                  .setTo(0)
                  .b
                  .copy$default$4,
                (com.softwaremill.quicklens.PathModify.apply[com.softwaremill.quicklens.x.C, scala.AnyVal](
                  c,
                  (
                      (`x₉`: scala.Function1[scala.AnyVal, scala.AnyVal]) =>
                        (c.copy(b =
                          c.b.copy(
                            a1 = `x₃`.apply(c.b.a1),
                            c.b.copy$default$2,
                            c.b.copy$default$3,
                            c.b.copy$default$4,
                            c.b.copy$default$5
                          )
                        ): com.softwaremill.quicklens.x.C)
                  )
                ): com.softwaremill.quicklens.PathModify[com.softwaremill.quicklens.x.C, scala.AnyVal])
                  .setTo(0)
                  .b
                  .copy$default$5
              )
          ): com.softwaremill.quicklens.x.C)
  )
}