1

I have following class structure which represents simple tree. Each item can have multiple children and parent.

The tree root is causing me a headache though. I'm trying to do this without using null so I can traverse the tree upwards by calling item.parent. To simplify it I want the root to has itself as a parent, but I cannot figure out how to do it.

interface Item {
    val parent: Directory
}

interface ItemWithChildren{
    val children: MutableList<Item>
}

class Directory() : Item, ItemWithChildren {
    override val children: MutableList<Item> = mutableListOf()
    override val parent: Directory by lazy { this }

    constructor(par: Directory) : this() {
        parent = par //Error: val cannot be reassigned 
    }
}

class File(override val parent: Directory) : Item

That code doesn't compile, because it's not possible to reassign the val parent. But using this as a default parameter value is also not possible. Is there any way out?

If I allow the parent to be nullable, then the solution is easy. But I don't wanna use the nulls, if possible. Also null would defeat the item.parent chain.

jnovacho
  • 2,825
  • 6
  • 27
  • 44

3 Answers3

3

You can use an init block. e.g.:

class Directory(parent: Directory? = null) : Item, ItemWithChildren {
    override val children: MutableList<Item> = mutableListOf()
    override val parent: Directory

    init {
        this.parent = parent ?: this
    }
}

Alternatively you could create a separate "parent" implementation for "root". e.g.:

interface ChildItem /* renamed from `Item` for clarity */ {
    val parent: ParentItem
}

interface ParentItem /* renamed from `ItemWithChildren` for clarity */ {
    val children: MutableList<ChildItem>
}

class Root() : ParentItem {
    override val children: MutableList<ChildItem> = mutableListOf()
}

class Directory(override val parent: ParentItem) : ChildItem, ParentItem {
    override val children: MutableList<ChildItem> = mutableListOf()
}

class File(override val parent: ParentItem) : ChildItem

This way your "root" item doesn't have a parent property similar to how your "leaf" ("file") items don't have a children property. You would likely also want to make your ChildItem and ParentItem interfaces extend a common interface (e.g. named Item).

mfulton26
  • 29,956
  • 6
  • 64
  • 88
  • 1
    Thanks, the separate implementation of root is actually what I needed. It will allow me to simplify my code more. – jnovacho Sep 24 '16 at 19:52
1

@mfulton26 answered how to do this in the way you strictly requested. But for others that may wonder about this choice, they should still also consider null values being OK for this type of work in Kotlin.

You can have a null property and a few derived properties that allows access asserted as not null. Because either way (your plan to avoid null or accepting and using null) you are going to have to ask "do I have a parent?" which is almost the same as asking "is parent null?" So why do a uncommon "possibly endless loop causing" work-around for this case?

If my tree class were something like:

data class Something(val text: String, val parentOrNull: Something? = null) {
    val parent: Something get() = parentOrNull!!
    val hasParent = parentOrNull != null
}

Then I have options of how to access the parent with and without worrying about the null:

val root = Something("rooty")
val child = Something("youngun", root)
val leaf = Something("baby", child)

fun printPathToNode(node: Something) {
    // use derived properties to check and not worry about the null
    if (node.hasParent) printPathToNode(node.parent)
    println(node)
}

fun findRoot(node: Something): Something {
    // use null operators to not worry about the null
    return node.parentOrNull?.let { findRoot(it) } ?: node
}

Then you can see it runs fine with good output, and no problems with null:

printPathToNode(leaf)    // rooty, youngun, baby
printPathToNode(child)   // rooty, youngun
printPathToNode(root)    // rooty

println(findRoot(leaf))  // rooty
println(findRoot(child)) // rooty
println(findRoot(root))  // rooty

Nulls should be avoided in cases where they do not make sense. But sometimes they are actually a reasonable option. Kotlin helps to protect you when you have nullable values as well by knowing about them and not just pretending all is OK. And then it gives you nice nullability operators to help you in working with them.

Community
  • 1
  • 1
Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • Thanks for the insight! To adamantly reject the null is not the right mindset. I was "lured" by the idea of Optional datatype which is not present in Kotlin stdlib. But in my case, that would not actually acomplish anything more useful than the null itself. – jnovacho Sep 24 '16 at 19:58
1

That's how I would use the @mfulton's answer:

class Directory(parent: Directory? = null) : Item, ItemWithChildren {
    override val children = mutableListOf<Item>()
    override val parent = parent ?: this
}
voddan
  • 31,956
  • 8
  • 77
  • 87