Position based declaration destructuring

TLDR; I got confused from the documentation. Moving the Person example away from the top to emphasize Kotlin de-structuring is meant for Pair/Entry type situations could be a big help.

I also find the documentation here misleading, and landed on this page after confusion there. The top example is definitely misleading without showing the Person constructor.

The position-based destructuring’s primary use-case is to work with mostly structural types like Pair and Triple ,

If this is the case, the documentation should begin with such an example. Also perhaps the documentation should explicitly mention de-structuring is positional. I only inferred this after reading the component1() component2() convention details.

To the point of symmetry with data class constructors. While mostly true, they aren’t quite symmetric to me; if a data class is refactored to add a first parameter, compilation of constructor parameters will break. But the now-invalid de-structuring will compile silently and still “work”, causing a particularly nasty type of error. This feels more unsafe. Additionally, it’s easier to look up construction sites during refactor (and more straightforward) than de-structure sites. It’s also more common to explicitly mark types in constructors than in de-structures, so there may be less type safety.

Considering that, maybe the documentation should even suggest the with(…)(…) syntax mentioned here for de-structuring Data classes with many, potentially changing fields like server response objects. Leave de-structuring to Tuples where positional thinking is implicit.

There is no need to remove position-based destructuring. We should simply add name-based destructuring too. JavaScript has both, to a great success.

It’s only unfortunate that position-based uses (). is a much better choice.

// Position-based
val [a, b] = pair
// Name-based
val {first, second} = pair

The benefit of using parens (instead of square brackets) is that it tends to mirror the syntax for creating the object (whether via a constructor or factory method). For example:

val l = listOf(1, 2, 3)
val (a, b, c) = l

You’re basically saying it should mirror a syntax for function/constructor calls. That argument makes zero sense to me.

What if I do:

val p = Pair(second = 2, first = 1)

or:

val p = 1 to 2
2 Likes

I stumbled across another example that seems to support name-based destructuring as well as provide a real use-case per your request, I’ll try and also provide some thoughts from my experience with Kotiln destructuring. cc @elizarov

The Use-Case:
Specially, I have been writing Typescript for the past couple years (alongside Kotlin) and discovered that in Typescript I was able to destructure ONLY the fields I need/want, which was a huge convenience, especially when you have large objects.

However, in Kotlin, today I learned that I had to add “" in for each field I wanted to skip, which isn’t feasible given the number of fields, and how often we move them around.) I then also realized the "” was also likely used as a result of destructuring being position-based.

Unfortunately all this resulted in me not using destructuring and just creating 10 local variables.

Thoughts:
I understand the symmetry arguments, and of course do not fully understand all the underlying design decisions so please forgive my ignorance! :slight_smile: I think people when using destructuring are simply trying to avoid typing someObjectName.propertyOne where they type someObjectName over and over, or to avoid creating a ton of local variables like val propertyOne = someObjectName.propertyOne, as such, I think the implementation should likely reflect that. I think forcing the user to also consider field-order, which is not so common concern in programming outside of serialization perhaps, is what has created the perceived burden. It’s also very difficult to reason with how to move fields around or when to add/remove an “_”. I encountered this when trying to figure out why my destructuring wasn’t working as expected. And finally it’s just really tedious adding 15 "_"s off a database pojo when i’m wanting 4 fields for a math calculation.

The difference is:

desired:

val (
    status, 
    totalSharesReserved, 
    totalSharesTransacted, 
    totalShares, 
    sharePrice, 
    minTransactionAmount
) = offering

actual:

val (_, _, _, status, _, _, _, _,
     _, _, _, _, _,
     minTransactionAmount, sharePrice, totalShares, totalSharesTransacted, 
     totalSharesReserved
) = offering // i was able to omit 17 other fields after totalSharesReserved, but if I ever need to destructure any of those variables then it will be more painful.

Recap:
I think name-based ordering and not forcing the use of “_” to skip fields (which i think is “free” if you switch to name-based ordering), will provide a better Kotlin user experience, at least for myself.

Thanks!

I think you are making your code extremely fragile by relying on the order of fields. So I understand why you would want to use named destructuring. It would need a different syntax though.

Position-based destructuring is best used for very stable types, I think.

You can build named destructuring yourself if you do not mind a bit of extra typing. This way your code is decoupled from Offering. It will only break if the fields you are interested in are changed in an incompatible way:

class Offering {
    val field1 = 0
    val totalShares = 1
    val field2 = 2
    val field3 = 3
    val totalSharesReserved = 4
    val field4 = 5
    val totalSharesTransacted = 6
    val field5 = 7
    val field6 = 8
    val field7 = 9
    val minTransactionAmount = 10
    val field8 = 11
    val field9 = 12
    val status = 13
    val field10 = 14
    val sharePrice = 15
    val field11 = 16
    val field12 = 17
    val field13 = 18
}

fun main(args: Array<String>) {
    val offering = Offering()

    data class FieldsToExtract(
        val status: Int,
        val totalSharesReserved: Int,
        val totalSharesTransacted: Int,
        val totalShares: Int,
        val sharePrice: Int,
        val minTransactionAmount: Int
    )

    offering.run { FieldsToExtract(status, totalSharesReserved, totalSharesTransacted, totalShares, sharePrice, minTransactionAmount) }.run {
        println(status)
        println(totalSharesReserved)
        println(totalSharesTransacted)
        println(totalShares)
        println(sharePrice)
        println(minTransactionAmount)
    }
}

We can actually do it easier with existing language features. The following is valid:

val (
    status, 
    totalSharesReserved, 
    totalSharesTransacted, 
    totalShares, 
    sharePrice, 
    minTransactionAmount
) = offering.run { Tupple6(status, 
    totalSharesReserved, 
    totalSharesTransacted, 
    totalShares, 
    sharePrice, 
    minTransactionAmount) }

Where Tupple6 is a simple tuple with 6 elements. Of course it would be possible to use some functions to make it somewhat tidier. Note that this code doesn’t have position issues. If you repeat this frequently for the same type, you can of course write a specific function to make it more elegant.

1 Like

Thanks for the responses. Those solutions make sense, but I can’t shake the feeling that this is a lot of extra work to put Kotlin users through when all they are likely wanting to do is quickly shorthand access variables.

Also considering how destructuring of variables is often used, creating a new data class instance also doesn’t seem ideal. It also doesn’t easily account for all the different combinations of fields that you may wish to destructure and you’ll find yourself in practice creating FieldsToExtractForFunction001, FieldsToExtractForFunction002, etc (obviously with more sensible names)

My offering object above is used in at least 10 functions, each looking at difference slices of the object (it’s a medium sized DB model)

I’m just trying to do:

val (
    status, 
    totalSharesReserved, 
    totalSharesTransacted, 
    totalShares, 
    sharePrice, 
    minTransactionAmount
) = offering

instead of

val status = offering.status
val totalSharesReserved = offering.totalSharesReserved
val totalSharesTransacted = offering.totalSharesTransacted
val totalShares = offering.totalShares
val sharePrice = offering.sharePrice
val minTransactionAmount = offering.minTransactionAmount

And considering the alternative to destructuring isn’t THAT much extra work/code, any extra work a user has to do to make destructuring work, I think will result in it not being used, at least not for anything other than trivial cases (objects with a 2-3 fields), but destructuring really shines for objects with a lot of fields.

Though, I still think it comes down to my original hypothesis that I just don’t think users care about field order when destructuring variables in practice (outside of serialization), which seems even more unnecessary given we already refer to them by name.

The shortest way to use the object (but with a different approach) is to just use: with(offering) { /* access as if local */ }. Of course if you need copies the story is more complex, but in the base case, you can have multiple receivers in Kotlin, you just can’t define functions that take them (yet).

1 Like