Hi there! What do you think about the collection b...
# stdlib
i
Hi there! What do you think about the collection builder functions,
buildList
,
buildSet
and
buildMap
, introduced as experimental in 1.3.70? Do you use them in your code? What are your impression of them?
t
I use
buildList
a lot in my code now, it is really useful. The code looks much better with it. I don’t really use the other two build functions for Set and Map.
8
z
I use
buildList
and
buildMap
a lot while I almost don't use
buildSet
. Some
buildMutable...
functions may also be helpful but they can be done with specifying implementation:
ArrayList().apply { ... }
. However, there are both mutable and read-only analogues for
listOf
,
mapOf
, so it is better to have both ones here too.
w
I use
buildList
a lot. I like it mostly, although I hear some complaints that it’s less readable than
Copy code
listOf(firstElement)
  + secondElement
  + thirdElement
and it’s easy to have an expression inside but forget to wrap it in an
add(…)
call. With
+
from the example above it’d be much more difficult to unintentionally not add something
d
I've used buildSet just once.
n
Couple of points: • when we have
buildList
, we should also have the other primary constructs like sets and maps handled for consistency. Which brings up the question about `buildArray`: do we need that? • there is a sweet-spot for buildXXX where the block is complicated enough to e.g. require some variables or statements but not so complicated that we loose track of what an
add
or
put
call operates on. I sometimes wished I could use
val l = buildList { mylist -> ...; myList.add(...) }
l
It's a little annoying that you need to write
this
to use the
+=
operator, or use
add
, but I still like
buildList
and used it in a few places, though the experimental status didn't encourage me to use it more.
z
I like them and wish JB would back the default listOf(), mapOf(), etc by them
True immutability is a nice win
d
We also use them quite a bit, and migrated some older code to them which helped make it look much cleaner!
m
I’ve barely used or felt the need for the new
build*
functions. • I rarely have the need. Most lists etc. are created by function chains anyway. • In cases when I’ve tried the builders they made code more difficult to read rather than easier. Partially because thee are
add()
calls scattered around and you don’t immediately see the receiver. • In many cases it feels wrong to introduce a scope and indent code just to create a list. The list is often a side-effect or (partial) result of another primary intent. Indenting it focuses the code on creating a list, not of the primary intent. For
buildString
it’s different somehow. Maybe because it’s way more common? And
append
is clearer in such contexts? And creating the String is usually the primary intent of the code within the block. I’ve run a quick search over my entire Kotlin codebase. There isn’t a single instance left where I use it. I guess there are good cases for these builders. I just haven’t found some in my back-end & front-end code yet.
f
@Zhelenskiy I added mutable versions in the original PR but they were rejected, don't remember the reasoning. I'd like to use them more often but the experimental status which is a result of the experimental type inference for builders is a blocker. It also fails with
buildMap
very often.
@wasyl the main advantage of the
build*
functions over what you have is that they take the expected size and allow you to build collections in the most efficient way without resizing. They are not meant to contain complex logic (but obviously can). A situation where I like to use them is in conjunction with `vararg`:
fun f(s: String, vararg ss: String) = buildList(1 + ss.lenght) { add(s); addAll(ss.toIterable()) }
w
Yes, I’m aware 🙂 My comment was from usability standpoint, and with small lists I don’t think there’s huge difference in peformance
z
@nkiesel • Is
buildArray
different from just
Array
constructor? Don't think so. You have to specify size and values per index. You have no
add
method for `Array`s. • You can do the following:
Copy code
val l = buildList {
   val myList = this
   myList.add(...)
}
f
@wasyl correct, there's a minimum size and small lists won't benefit.
j
@ilya.gorbunov will they be experimental in 1.5 too?
i
Yes, collection builders will have to remain experimental in 1.5 because they rely on the experimental builder type inference. We plan some changes in the latter in order to stabilize it and they will likely affect the usages of collection builders as well.
j
renaming them is not going to happen no?
i
No plans for that.
😌 1
👍 2
l
I'd like to use
buildList
in a Gradle plugin and possibly libraries, but I'm not sure if it's safe when it comes to forward compatibility since the RequiresOptIn annotation only says it's experimental overall.
I think I'd like to have the unary plus operator in the scope of the
buildList
lambda, because of that annoying indent:
Copy code
buildList {
    if (whatever) add(
        Stuff(
            ...
        )
     )
    ...
}
with unary plus, it becomes the following:
Copy code
buildList {
    if (whatever) +Stuff(
        ...
     )
    ...
}
I guess that's what the unary plus operator has been designed for, and there's a precedent in kotlinx.html. Is that something that was ever considered for
buildList
and friends @ilya.gorbunov?
f
I generally think that it was a bad call to make the receiver
this
in all of the
build
functions as it is a source of many bugs. Having the receiver as first argument would automatically solve your issue as well since you'd have
it + Stuff
d
Debatable
l
I'd say… depends on the developer and their mental condition at the time of writing the code 😄
f
Luckily I developed a keen eye for this, just had such a case again. It always happens in extension functions and follows a similar pattern:
Copy code
fun String.doSomething() = buildString {
    forEach { /* or something similar that is available on both String and StringBuilder */ }
}
Solution:
Copy code
fun String.doSomething(): String {
    val it = this
    return buildString {
        it.forEach { /*...*/ }
    }
}
I stand by what I said above, this would avoid these issues altogether:
Copy code
fun String.doSomething() = String { it ->
    forEach { c -> it.append(c).append(c) }
}
z
@Fleshgrinder do you think it is better to have some way to explicitly name receivers in lampldas?
f
I don't think anything would help here, other than not having
StringBuilder
as receiver of
buildString
. The problem is that the sources often have the same functions, and this confuses people. I already see the same happening with
buildList
and friends.
Copy code
fun <E> Iterable<E>.doSomething() = buildList { forEach { /* ouch */ } }