scala-newtype icon indicating copy to clipboard operation
scala-newtype copied to clipboard

Illegal cyclic reference error when abstract companion

Open dmitry-worker opened this issue 4 years ago • 5 comments

I suppose the companion objects aren't working as expected when they have a type hierarchy. Consider a simple example:

  1. I have some re-usable companion:
abstract class AbstractObjectCompanion[T] {
  def apply(src: Int): T
  implicit class Ops(t: T) {
    def print = t.toString
  }
}
  1. And its implementation here:
import io.estatico.newtype.macros.newtype

package object some {
  @newtype class MyInt(val i: Int)
  object MyInt extends AbstractObjectCompanion[MyInt] {
    override def apply(src: Int): MyInt = new MyInt(src)
  }
}

... does not compile:

illegal cyclic reference involving type MyInt
  object MyInt extends AbstractObjectCompanion[MyInt] {

But it compiles without @newtype OR without AbstractObjectCompanion.

dmitry-worker avatar Feb 15 '21 13:02 dmitry-worker

Out of interest I looked at this, the macro desugars to:

package object some {
  type MyInt = MyInt.Type
  object MyInt extends AbstractObjectCompanion[MyInt] {
    type Base = Any { type __MyInt__newtype }
    abstract trait Tag extends Any
    type Type <: Base with Tag
  }
}

(ignoring other details) which is just cyclic. I guess Base/Tag could be obfuscated a bit (__MyInt__ prefixed) and moved out, and just define the external Type.

I'm guessing that the compiler must have ways to deal with this when dealing with companion classes and companion objects in a cyclical relationship..

dwijnand avatar Feb 15 '21 13:02 dwijnand

That begs the question, have you tried the following?

abstract class AbstractObjectCompanion {
  type Type
  def apply(src: Int): Type
  implicit class Ops(t: Type) {
    def print = t.toString
  }
}

joroKr21 avatar Feb 15 '21 17:02 joroKr21

Sorry to bump this old issue, but just ran into this exact situation. I want to extract some boilerplate fromString[...] functions into a base class for pretty much the same reason as the OP, and I get the illegal cyclic reference error.

~I tried using a type member as @joroKr21 suggested, and it seems to work, btw! But now it's slightly more ugly :)~ I misread, and apparently making it a type member is even cleaner! Thanks @joroKr21!

@newtype final case class Foo private (x: String)
object Foo extends MyBase(...)

abstract class MyBase(...) {
  type Type

  def fromString(s: String)(implicit ev: Coercible[String, Type]): Either[String, Type] = 
     alphanumeric(s).map(_.coerce[Type])
}

(using abstract class here to pass additional ctor parameters needed for other things)

This gives me:

> Foo.fromString("5")
Right(5)

as expected.

Leaving this here for anyone who would run into this issue...

hmemcpy avatar Dec 20 '21 20:12 hmemcpy

p.s. could not name the type parameter Type because it clashed with what the macro expands to :)

That's the whole point, if you leave it abstract Type will be defined by object Foo This works for me: https://scastie.scala-lang.org/eQwfMXkJQA252pPolKxUHQ

joroKr21 avatar Dec 21 '21 10:12 joroKr21

D'oh! Of course :) Haha this is even better! Thanks @joroKr21!

hmemcpy avatar Dec 21 '21 11:12 hmemcpy