Explanation for "illegal cyclic reference" involving implicits
Asked Answered
W

1

7

In my project, I have a type A, used for arguments in a few places, where I want a bunch of types automatically converted to that type. I've implemented this using a few implicit classes in the companion object of A. I've removed everything not necessary to trigger the problem:

trait A
object A {
  implicit class SeqA[T](v: Seq[T])(implicit x: T => A) extends A
  implicit class IntA(v: Int) extends A
  implicit class TupleA(v: (Int, Int)) extends SeqA(Seq(v._1, v._2))
}

But scalac rejects this code because of an illegal cyclic reference:

$ scalac -version
Scala compiler version 2.12.8 -- Copyright 2002-2018, LAMP/EPFL and Lightbend, Inc.
$ scalac A.scala 
A.scala:5: error: illegal cyclic reference involving class TupleA
  implicit class TupleA(v: (Int, Int)) extends SeqA(Seq(v._1, v._2))
                 ^
one error found

What exactly is the problem here? What is the compiler tying to do/infer/resolve that involves an illegal cylce?

Bonus points for a valid way to implement these implicit classes.

Wigwag answered 21/2, 2019 at 10:3 Comment(0)
D
10

You're running into SI-9553, a three-and-a-half-year-old compiler bug.

The reason the bug hasn't been fixed is probably in part because there's an extremely simple workaround—just put an explicit type parameter on the generic class you're extending:

trait A
object A {
  implicit class SeqA[T](v: Seq[T])(implicit x: T => A) extends A
  implicit class IntA(v: Int) extends A
  implicit class TupleA(v: (Int, Int)) extends SeqA[Int](Seq(v._1, v._2))
}

This is probably a good idea anyway, since you're asking for trouble any time you let type parameters get inferred where implicit definitions are involved.

As a side note, you can investigate issues like this with a compiler option like -Xprint:typer. In this case it shows the following in a REPL:

// ...
implicit class TupleA extends $line6.$read.$iw.$iw.A.SeqA[Int] {
  <paramaccessor> private[this] val v: (Int, Int) = _;
  def <init>(v: (Int, Int)): $line6.$read.$iw.$iw.A.TupleA = {
    TupleA.super.<init>(scala.collection.Seq.apply[Int](v._1, v._2))({
      ((v: Int) => A.this.IntA(v))
    });
    ()
  }
};
implicit <synthetic> def <TupleA: error>(v: (Int, Int)): <error> = new TupleA(v)

Which in this case isn't terribly helpful, but it at least indicates that the problem is happening in the definition of the synthetic conversion method for the TupleA implicit class, and not at some point before that.

Dulcle answered 21/2, 2019 at 10:38 Comment(1)
Thank you very much! With this knowledge I was able to fix this problem in my actual code. I added the type parameter on SeqA after I already had TupleA, that's why I didn't notice that a type parameter was inferred there.Wigwag

© 2022 - 2024 — McMap. All rights reserved.