Arrays-2.0, multiple varargs and collection literals


#1

Hello,

I’m very big fan of Kotlin and it’s syntax improvements over Java (such as zero-boilerplate delegation and zero-boilerplate singletones).

Recently I got interested in zero-boilerplate builders.
I believe that builder pattern exists to overcome following language limitations:

  • lack of named arguments
  • lack of default and optional arguments
  • lack of multiple vararg arguments

And when first two limitations don’t exists in Kotlin I’ll try to resolve third.

Also I want to share idea how to implement collection literals.
It’s separate feature, but have common component with vararg - arrays.

Arrays - will be focus of this article.


NOTE:

Maybe this plan is too complex or idealistic to be implemented.
I split it to parts and some can be skipped\changed to preserve backwards compatibility or keep simplicity.
At least if it inspire someone I’ll be happy.

Also my English is weak. Sorry about that


These are steps to do:

  1. Rename Array<T> to [T].
  • because it makes array types special like lambda types and it’s important on next steps
  • it should look like this:
   val arrayDeclarationExample: [String]? = null
   val genericsProjectionsExample: [out String]? = null
   val ThreeDimensionalExample: [[[Int]]]? = null
   fun arrayParameterExample(array: [Byte?]) { }
   fun arrayInLambdaExample(block: (() -> [Long])) { }
   fun arrayInLambdaExampleReceiver(block: [Char].(([Char]) -> Unit) { }
   fun getStringArray(): [String] = ["String"]
   fun getIntArray() = [1]
  1. Imlement array literals
  • because it’s useful, wanted feature and it’s required on next steps
  • proposed syntax is:
   val arrayLiteralExample = ["hello", "array"]
   val arrayLiteralExampleEmpty: [String] = []
   val arrayLiteralExampleThreeDimensional = [[[]], [[], []], []]  //Any
   arrayParameterExample([42, 42])
   arrayParameterExample([])
   arrayInLambdaExample({ [] })
   arrayInLambdaExample({ [1, 2, 3] })
   arrayInLambdaExample { [1, 2, 3] }
  1. Introduce vararg arrays instead of vararg keyword
  • difference with varargs is that multiple vararg array arguments are allowed
  • difference with arrays is that spread operator is required when array is created not in place
  • Java varargs are converted to Kotlin vararg arrays automatically
  • syntax should look like this:
   fun takesVarargArrays(a: vararg [Int], b: vararg [String])
   takesVarargArrays([1,2,3], [])
   takesVarargArrays(*getIntArray(), *getStringArray())
  1. Relax syntax rules on vararg arrays to encourage users to use them in right places
  • 1) vararg array arguments are default to empty arrays
  • 2) braces on vararg array literal can be ommitted with single value
  • and here are examples of legal expressions:
   takesVarargArrays()
   takesVarargArrays([])
   takesVarargArrays(b = [])
   takesVarargArrays(a = 1)
  1. Apply last argument convention for vararg array literals
  • it works well for higher-order functions and makes Kotlin so awesome!
  • also, it’s known and simple rule - no re-inventing the wheel, no surprises
  • example
   fun lastArgumentConventionExample(dummy: String, a: vararg [Int]) {  }
   lastArgumentConventionExample("text") [1, 2, 3, 4, 5]
   fun lastArgumentConventionExampleSingle(a: vararg [Int]) {  }
   lastArgumentConventionExampleSingle [1, 2, 3, 4]
  • collection literals now are very easy to implement, some are already implemented
   val listExampleGeneric = listOf<String> []
   val listExampleGenericSameAsPrevious = List<String> []
   val mapExample = Map [
         0 to false,
         1 to true
   ]
   val arrayListExample = ArrayList ["constructor", "inferred", "from", "JVM"]
   val arrayListExampleHint = ArrayList(4) [""]
   val rxExample = Observable.just [1, 2, 3]
  1. Update index access and index assign operators to work after dot
  • this should resolve some inconsistencies with changes introduced in previous steps
  • example
   val indexAccessExample = arrayListExample.[0]
   ArrayList[1, 2, 3].[0] = 3
   Map<String, Any>[].["key"]

And result should be following:

annotation class AnnotationExample(val args: vararg [String])
@AnnotationExample[ "hello", "annotation" ]
@get:AnnotationExample
@set:[AnnotationExample(args = "single")]
val annotationUsageExample = 1


class WithoutBuilder(
   val name: String,
   val array: [String],
   val optional: Int?,
   val variadic: vararg [String],
   val nested: [WithoutBuilder]
)
val withoutBuilderExample = WithoutBuilder("exampleParent",
   array = [],
   optional = 100,
   nested = WithoutBuilder("exampleNested"
      array = ["", "", ""],
      variadic = "single",
      nested = WithoutBuilder("exampleNestedDeep", [], 100,
         variadic = ["a", "b", "c"],
         nested = WithoutBuilder("exampleNestedLast",
            array = getStringArray(),
            variadic = *getStringArray())
      )
   )
)


#2

Actually I also would like to have array literals, just like in vanilla Java.

But regarding varargs a case is a little more complex:

fun takesVarargArrays(a: vararg [Int], b: vararg [String])
takesVarargArrays([1,2,3], [])

This one maybe seems quite oblivious, because you have concrete types.

But if you had changed it to this:

fun takesVarargArrays(a: vararg [Any], b: vararg [Any])
takesVarargArrays([1,2,3], [])

Then it becames ambigous wherever you want to pass a whole arrays as arguments or just their values. That’s why spread operator was introduced in Kotlin.

takesVarargArrays(*[1,2,3], *[])

But even this one is off, because you can spread multiple arrays into one vararg argument, so this one is also wrong, because in Kotlin if you write:

fun takesVararg(vararg a: Int)
takesVararg(1, *intArrayOf(2, 3, 4), 5, *intArrayOf(6, 7, 8))

Then is’s a completelly valid code which applies only to a single vararg argument :slight_smile:

Last argument convension seems a little off to me, because if you write code in classic braces you have almost indentical amount of characters. Another problem is that it becames ambigous in some cases. If you returned array/collection from such function:

fun varargFunction(vararg numbers: Int) = numbers
varargFunction [1]

Now it’s a problem wherever you would like to pass value 1 as argument, or you want to call array returned by it’s index :slight_smile:

Collection literals also don’t seem so attractive, because you already have functions in Kotlin which are useful enough for the most tasks. And there are also immutable variants. Here you are forced to operate on mutable collections.

As for the rest I don’t have an opinion :slight_smile:


#3

Okay,

some things are described bad due to my language limitations and some because I was trying to make proposal as simple as possible.
So, need to clarify: vararg arrays are always passed as values and always created and declared explicitly.
Here is example of behavior in case with array of Any you described:

fun takesVarargArrays(a: vararg [Any], b: vararg [Any]) 
    = println("a.size=${a.size}, b.size=${b.size}")

takesVarargArrays([[]], [])     // a.size=1, b.size=0

On JVM when you passes whole array to varargs of Any you wraps array in array and passes it to function - here it’s visible.
Outer array is vararg array used in function. Inner is an content of vararg array.
Everything is straightforward.


If we go deeper, we can even get rid of * and vararg operators (that are just syntax sugar for arrays).
I.e. Rust language don’t have varargs and I really like this decision
because that same functionality can be achieved with following syntax:

fun takesArray(a: [Int] = []) 
    = println("a.size=${a.size}")

takesArray()                    // a.size=0
takesArray([])                  // a.size=0
takesArray([1] + [1, 2])        // a.size=3
val array = [1, 2]
takesArray(array)               // a.size=2
val value = 1
takesArray(array + [1, value])  // a.size=4

that don’t introduces additional complexity to parser and not confuses novices with extra operators.
As you see, spreading multiple arrays can be easily replaced with array concatenation.

Problem is only that currently Kotlin creates defensive copy of array with *, and I’m not sure if it’s safe to not create it.


About last argument convention: it makes syntax even simpler:

takesArray [ 1, 2, 3 ]

and it’s not ambiguous (see step 6. sorry if it was unclear before edit).
It should simplify collection creation and building complex objects.

As for collection factory functions, you will be able to use them in this way:

listOf [1, 2, 3]
mutableListOf [1, 2, 3]
mapOf [
    1 to 2,
    2 to 3]

But they don’t look like collection literals (on top of Kotlin feature survey).
I propose to deprecate these functions and construct collections like these:

List [1, 2, 3]
MutableList [1, 2, 3]
Map [
    1 to 2,
    2 to 3]

Don’t sure it will be implemented as constructors or uppercase functions…
But same collection types remain and immutablility will be on first place (confusing example in 5 also updated).


Thanks,
I’m really didn’t know about multiple arrays spreading and missed case of [Any],
good catch!