Search code examples
constructorkotlinimmutabilitydata-class

Multiple constructors on an immutable (data) class


I'm trying to implement an immutable data class with more than one constructor. I felt that something like this should be possible:

data class Color(val r: Int, val g: Int, val b: Int) {
   constructor(hex: String) {
        assert(Regex("#[a-fA-F0-6]{6}").matches(hex), { "$hex is not a hex color" } )
        val r = hex.substring(1..2).toInt(16)
        val g = hex.substring(3..4).toInt(16)
        val b = hex.substring(5..6).toInt(16)
        this(r,g,b)
    }
}

Of course, it isn't: Kotlin expects the call to the main constructor be declared at the top:

constructor(hex: String): this(r,g,b) {
    assert(Regex("#[a-fA-F0-6]{6}").matches(hex), { "$hex is not a hex color" } )
    val r = hex.substring(1..2).toInt(16)
    val g = hex.substring(3..4).toInt(16)
    val b = hex.substring(5..6).toInt(16)
}

That's no good either, as the call is executed before the constructor body and can not access the local variables.

I can do this, of course:

constructor(hex: String): this(hex.substring(1..2).toInt(16),
                               hex.substring(3..4).toInt(16), 
                               hex.substring(5..6).toInt(16)) {
    assert(Regex("#[a-fA-F0-6]{6}").matches(hex), { "$hex is not a hex color" } )
}

But this will check the assertion too late, and does not scale very well.

The only way I see to get close to the desired behaviour is this using a helper function (which can't be defined non-static on Color):

constructor(hex: String): this(hexExtract(hex, 1..2), 
                               hexExtract(hex, 3..4), 
                               hexExtract(hex, 5..6))

This does not strike me as a very elegant pattern, so I'm guessing I'm missing something here.

Is there an elegant, idiomatic way to have (complex) secondary constructors on immutable data classes in Kotlin?


Solution

  • As explained here, using the operator function invoke on the companion object (just like Scala's apply) one can achieve not really a constructor, but a factory that looks like a constructor usage-site:

    companion object {
        operator fun invoke(hex: String) : Color {
            assert(Regex("#[a-fA-F0-6]{6}").matches(hex),
                   {"$hex is not a hex color"})
            val r = hex.substring(1..2).toInt(16)
            val g = hex.substring(3..4).toInt(16)
            val b = hex.substring(5..6).toInt(16)
            return Color(r, g, b)
        }
    }
    

    Now, Color("#FF00FF") will to the expected thing.