Correct way to handle circular reference problem in kotlin

205 views Asked by At

I'm trying to build a simple json parser and below is my JSON type representation with a sealed class

sealed interface JValue {
    data class JString(val value: String): JValue
    data class JNumber(val value: Float): JValue
    data class JBool(val value: Boolean): JValue
    data object JNull: JValue
    data class JObject(val value: Map<String, JValue>): JValue
    data class JArray(val value: List<JValue>): JValue
}

val jValueArr: JValue = getJArrayValue()
val jValueString: JValue = JValue.JString("Test")
val jValueNumber: JValue = JValue.JNumber(100f)

val jArray = JValue.JArray(listOf(jValueString, jValueNumber, jValueArr))

fun getJArrayValue(): JValue.JArray = JValue.JArray(listOf(jValueArr))

fun main() {
    println(jArray.value)
}

The problematic parts are JObject and JArray as they can hold a JValue (circular dependency?)

If I run the above code it prints the below value as jArray

[JString(value=Test), JNumber(value=100.0), JArray(value=[null])]

as you see the third element is JArray(value=[null]) instead of jValueArr, but I can understand that it's null because the kotlin initializes the objects lazy and jValueArr won't be initialized while calling getJArrayValue().

My question is, how can I properly handle this scenario?

Link to my actual code - https://github.com/VenkateswaranJ/ParserCombinatorsKT/blob/master/src/main/kotlin/JsonParser.kt#L98-L130

Update:

I fixed it in my parser with some dummy forward reference (not sure it's the best way of doing it) Here is the code - https://github.com/VenkateswaranJ/ParserCombinatorsKT/blob/master/src/main/kotlin/JsonParser.kt#L16-L26

2

There are 2 answers

0
marcinj On

You can fix the circular initialization problem with lazy initialization of jValueArr:

val jValueArr by lazy { getJArrayValue() } 

unless you declare your variable lazy, it is initialized in the order of declaration.

Yet, you will still have a circular dependency issue, in your current design, your getJArrayValue function is creating a JArray with a list that contains jValueArr itself. This can lead to infinite recursion, as it's essentially a circular structure where jValueArr contains itself, which contains itself, and so on.

You should redesign your data to avoid endless recursion.

1
broot On

In most cases it is not possible to have a circular reference where both sides are read-only. We have to make at least one side mutable.

In your specific case we have a list, so we can initially create a mutable list and then make it read-only:

val jValueArr: JValue = run {
    val list = mutableListOf<JValue>()
    JValue.JArray(list).also { list += it }
}

Above solution may be considered not exactly safe, as we can cast the list back to MutableList and mutate it. Alternatively, we can create our own implementation of MutableList which doesn't allow mutating after switching it to read-only. Or we can use buildList() which provides such implementation already.

Please be aware you can't print data classes with circular references, because then you get into an infinite recursion. Also, it feels strange to use circular references in a data structure which seems related to JSON. JSON doesn't support circular references.