Search code examples
scalaclassrecursioncase

Can a value of a case class contain itself in Scala?


Given a tree of following shape:

enum Tree
  case Node(left: Tree, right:Tree)
  case Leaf

is it possible to create a value n:Node, such that n.left == n || n.right == n? val n:Node = Node(n,n) does not work, but is there a compile-time guarantee that no such value can exist? Is there such a guarantee (ie that a.a==a may never hold for any a:A where A is a case class) for case classes in general?

A bit of additional context: I came upon this issue because I need to know whether a collection of Trees shapes up to a single, well-formed tree, and at one point wanted to check, whether said collection has any Nodes that refer to themselves.


Solution

  • With class (and case class is not exception) you are eagerly evaluating every expression passed as an argument to the constructor. lazy or def will not help, because at the end of the day you have to obtain the reference, and you cannot obtain it before the constructor is run. Inside the constructor is actually the first time you obtain the reference to the (not yet fully constructed) object.

    You'd have to use var or runtime reflection to set this value:

    final case class Data(var data: Data)
    
    val data = locally {
      val recurData = Data(null.asInstanceOf[Data])
      recurData.data = recurData
      recurData
    }
    
    data.data == data
    

    (version with var)

    final case class Data(data: Data)
    
    val data = locally {
      val recurData = Data(null.asInstanceOf[Data])
      val field = recurData.getClass.getDeclaredField("data")
      field.setAccessible(true)
      field.set(recurData, recurData)
      recurData
    }
    
    data.data == data
    

    (version with reflection)

    If this was not a case class you could do some weird stuff in constructor's body - if you set the val yourself you could use some logic to decide where the reference comes from and this as reference would be accessible.

    class WeirdClass private (refE: Either[Unit, WeirdClass]) {
      def this() = this(Left(()))
      def this(ref: WeirdClass) = this(Right(ref))
    
      val ref: WeirdClass = refE.getOrElse(this)
    }
    
    val w = new WeirdClass()
    w.ref == w
    

    Normal case class, even with auxiliary constructors, would not let you do such thing with a val defined in a constructor argument list - vals from constructor argument list are initiated with the value you passed it and there is no way to customize it (even defaults are executed before the constructor is called) and auxilary constructors do not let you use this as anything other than something to call (this(this) results in compilation error).

    So as long as you are using normal case class, let Scala generate the vals definition and make sure that nobody uses vars nor reflection, it should never happen.