Cannot access width before superclass constructor has been called

I have the following code for a picross/nonogram hobby project:

data class Level private constructor(
    val solution: Set<Point>,
    val width: Int,
    val height: Int,
    val rowHints: Array<IntArray>,
    val columnHints: Array<IntArray>
){
    constructor(solution: Set<Point>) : this(
        solution = solution.toSet(),
        width = solution.maxBy(Point::xPos),
        height = solution.maxBy(Point::yPos),
        rowHints = determineRowHints(solution, width, height)
    )

    companion object {
        private fun determineRowHints(solution: Set<Point>, width: Int, height: Int): Array<IntArray>{
            //logic here
        }
    }
}

the problem is, I cannot access the primary constructor with a precalculated width. A possible solution is to do the calculation again, but that would be a waste of computational power. I want to access the width property I just calculated in the function defined after it.

I understand that I might not be doing a best practice approach, and would like to now what to do to solve this problem.

Option 1:

data class Level private constructor(
    val solution: Set<Point>,
    val width: Int,
    val height: Int,
    val rowHints: Array<IntArray>
){
    companion object {
        fun Level(solution: Set<Point>): Level {
            val solution = solution.toSet()
            val width = solution.maxBy(Point::xPos)
            val height = solution.maxBy(Point::yPos)
            val rowHints = determineRowHints(solution, width, height)

            return Level(solution. width, height, rowHints)
        }

        private fun determineRowHints(...): Array<IntArray>{
            //logic here
        }
    }
}

Option 2:

data class Level private constructor(
    val solution: Set<Point>,
    val width: Int,
    val height: Int,
    val rowHints: Array<IntArray>
)

fun Set<Point>.toLevel(): Level {
    val solution = solution.toSet()
    val width = solution.maxBy(Point::xPos)
    val height = solution.maxBy(Point::yPos)
    val rowHints = determineRowHints(solution, width, height)

    return Level(solution. width, height, rowHints)
}

private fun determineRowHints(...): Array<IntArray>{
    //logic here
}

Option 3 (use an intermediate constructor):

data class Level private constructor(
    val solution: Set<Point>,
    val width: Int,
    val height: Int,
    val rowHints: Array<IntArray>,
    val columnHints: Array<IntArray>
){
    constructor(solution: Set<Point>) : this(
        solution = solution.toSet(),
        width = solution.maxBy(Point::xPos),
        height = solution.maxBy(Point::yPos)
    )

    private constructor(solution: Set<Point>,
                        width:Int,
                        height:Int): this(
        solution = solution.toSet(),
        width = width,
        height = height,
        rowHints = determineRowHints(solution, width, height))

    companion object {
        private fun determineRowHints(solution: Set<Point>, width: Int, height: Int): Array<IntArray>{
            //logic here
        }
    }
}

This solution isolates all complexity in the class as implementation detail. It merely uses an intermediate private constructor to be able to name the width/height parameters.

Why not make the function an operator fun invoke(solution: Set<Point>)? Then it codes as normal.

I am not a fan of that. It surprises developers and there is not a big difference in code at the usage side, is there?

I don’t really understand why so many people dislike using invoke on companions. On the use side it works exactly like a normal constructor.
The only “surprise” is for someone looking at the implementation, but then it’s nothing more than a more convenient factory function. As long as the function has no side effects (which it shouldn’t have whatever it’s called) there should be no problem.

What is the benefit over using an ordinary function that has the same name as the class?

In which way does it “work exactly like a normal constructor” that is not also the case for the ordinary function?

In which way is it a “more convenient factory function” than the ordinary factory function?

On invocation side the syntax for calling both functions looks exactly the same syntactically, safe for the import. If the import is your argument, then make it a package function.

The difference is in the Java API. You can use it with @JvmStatic and @JvmName to create a normal factory function for use by Java clients. Otherwise the free function is better (but this example used a companion function in which case the invoke operator makes things look “normal” - I still prefer the intermediate constructor as the client doesn’t get to care about the implementation detail.

1 Like