Search code examples
kotlin

Omit function parameters on condition in Kotlin


If I have a function or constructor in Kotlin, how do I omit certain parameters with a conditional? For example:

val capture = ScaleCapture(
    if (jsonObject.has(JSON.ID))
        id = jsonObject.getInt(JSON.ID),
    if (jsonObject.has(JSON.WEIGHT))
        weight = jsonObject.getInt(JSON.WEIGHT)
)

//...

data class ScaleCapture(
    val id: Int = 0
)

What I want is to ensure that, rather than sending a null value for a given parameter, the parameter is omitted entirely at the call site, which allows the default value to be used. If a function has multiple parameters with default values, it would be cumbersome to write out every possible combination of available parameters:

// Example with 2 parameters with default values
// We cannot pass null values because this is not the same as an omission due to default parameter values

if (jsonObject.has(JSON.ID)) {
    if (jsonObject.has(JSON.WEIGHT)) {
        return ScaleCapture(
            id = jsonObject.getInt(JSON.ID),
            weight = jsonObject.getInt(JSON.WEIGHT))
    } else {
        return ScaleCapture(
            id = jsonObject.getInt(JSON.ID)
    } else if(jsonObject.has(JSON.WEIGHT)) {
        return ScaleCapture(
            weight = jsonObject.getInt(JSON.WEIGHT))
    } else {
        return ScaleCapture()
}

//...

data class ScaleCapture(
    val id: Int = 0,
    val data: Int = 0
)

How do I accomplish this? I checked the Kotlin documentation under default values, but there wasn't much information.


Solution

  • Just for the sake of argument, there are two other options.

    Mutable state

    Just go for mutable state:

    data class ScaleCapture(
        var id: Int = 0,
        var data: String = "",
    )
    
    val scaleCapture = ScaleCapture()
    if (jsonObject.has(JSON.ID)) scaleCapture.id = jsonObject.getInt(JSON.ID)
    if (jsonObject.has(JSON.WEIGHT)) scaleCapture.data = jsonObject.getString(JSON.WEIGHT)
    

    Reflection

    If you want the ScaleCapture to be immutable, you could also opt by going the full-fledged reflection way:

    data class ScaleCapture(
        val id: Int = 0,
        val data: String = "",
    )
    
    val parameters = mutableMapOf<KParameter, Any>()
    if (jsonObject.has(JSON.ID)) parameters.put(primaryConstructor.parameters.first { it.name == "id" }, jsonObject.getInt(JSON.ID))
    if (jsonObject.has(JSON.WEIGHT)) parameters.put(primaryConstructor.parameters.first { it.name == "data" }, jsonObject.getString(JSON.WEIGHT))
    
    val primaryConstructor = ScaleCapture::class.primaryConstructor!!
    val scaleCapture = primaryConstructor.callBy(parameters)
    

    My opinion

    I like immutability a lot, but using reflection is super dirty. Personally I would create two types. The first mutable and the next immutable:

    private data class MutableScaleCapture(
        var id: Int = 0,
        var data: String = "",
    )
    
    data class ScaleCapture(
        val id: Int,
        val data: String,
    )
    

    Then you can just map the mutable one to an immutable and expose that one to the rest of your application:

    val capture = MutableScaleCapture()
    // if (jsonObject.has(JSON.ID)) scaleCapture.id = <something>
    // .. etc
    
    ScaleCapture(capture.id, capture.data)