I often find I end up creating a pair of methods o...
# getting-started
r
I often find I end up creating a pair of methods on a class -
foo(vararg elements: Bar)
and
foo(elements: List<Bar>)
because sometimes it's convenient to use varargs, but sometimes I have a List and
*elements.toTypedArray()
is a bit of a faff. I tend just to make one delegate to the other - but I just encountered a case where it was pure waste to allocate the extra List or Array, as all I then did was iterate over it. Is there any way to abstract over
Array
and
List
to treat both as the same sort of thing?
Example code:
Copy code
fun foo(vararg elements: Bar) {
  for (element in elements) {
    callMethodThatUses(element)
  }
}

fun foo(elements: List<Bar>) {
  for (element in elements) {
    callMethodThatUses(element)
  }
}
It would be nice to have:
Copy code
fun foo(vararg elements: Bar) = doFoo(elements)

fun foo(elements: List<Bar>) = doFoo(elements)

private fun doFoo(elements: ???) {
  for (element in elements) {
    callMethodThatUses(element)
  }
}
b
Is array.asList() out of the question? since that just wraps the array (as opposed to toList which allocates a new chunk of memory to copy the elements into). Or perhaps have doFoo take an IteratorFoo and pass it array.iterator() or list.iterator()
r
It was the extra allocation of an object to wrap the array as a List or Iterator I was vaguely hoping to avoid - that's what I normally do, it just occurred to me that I could literally copy the method and void it entirely, but at the expense of the duplication.
y
Taking a page out of the FP book, here's a typeclass-esque solution: (I'm avoiding using iterators on purpose. Bear in mind that normal array and list iteration is optimised, but this version wouldn't be)
Copy code
abstract class ListLike<in L, out E> {
  abstract operator fun L.get(index: Int): E
  abstract val L.size: Int
  
  final inline fun L.forEach(block: (E) -> Unit) { for(i in 0..size) block(i) }
  
  object ListImpl: ListLike<List<*>, Any?>() {
    override operator fun List<*>.get(index: Int) = this[index]
    override val List<*>.size = size
  }
  object ArrayImpl: ListLike<Array<*>, Any?>() {
     override operator fun Array<*>.get(index: Int) = this[index]
    override val Array<*>.size = size
   }
  
}
(Code untested because I'm on mobile) Simply just bring in the apt XImpl instance using
with
, and the function with the real implementation should have
ListLike<L, E>
as its receiver (ideally a context receiver if you're on the bleeding edge), or it can just take it as a parameter and bring it in itself. You might need some unsafe casts here or there, which can simply be wrapped in a function on ListLike's companion.
In general, you can abstract over some types A, B, C, by coming up with a common interface between them, modifying it so that it takes a receiver for every method, creating implementations of it for the types A, B, C (and maybe some companion funs to hide the nasty unsafe casting), and then simply write your method so that it has a receiver of the common interface, and the code inside your method will look practically the same. It's a shame we don't have some automagical system to bring in our implementations, like the Rust traits system for instance, but we can make do.
e
after https://youtrack.jetbrains.com/issue/KT-43871 you could choose to keep only the
List
version, and make the callers write
foo([1, 2, 3])
👍 1
or in the far future, https://youtrack.jetbrains.com/issue/KT-57266#focus=Comments-27-6964927.0-0 suggests that
varargs
might not be backed by arrays anymore, and then perhaps the alternative will not require wrapping into a List
👍 1
s
I was so sure that Array<T> implements Iterable<T> but it doesn't 😮🤯 https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/
e
arrays are an intrinsic part of the JVM's object model which are deeply special. Kotlin can't change that
Copy code
arrayOf<Number>() as Array<Int>         // throws ClassCastException
arrayOf<Int>() as Array<Number>         // succeeds as if it were covariant, even though a mutable container should really be invariant
(arrayOf(1) as Array<Number>)[0] = 1.0  // throws ArrayStoreException
this is different than how all modern generics work, because of JVM behavior pre-dating Java 1.0
s
good point. Purely academical "hindsight is 20/20" question: would it have been a better idea to make
varargs
a List instead of an Array? Was an Array chosen for historical reasons or for efficiency or is there another reason?? @Rob Elliot so
List
and
Array
don't have a joint interface, but both can produce an
Iterator<T>
, so you could use that as a unifying solution!?! You won't have a
for-loop
(that one works on
Iterable<T>
) but you do have
forEach
and
map
.
c
Was an Array chosen for historical reasons
I believe it was for interoperability with Java's varargs, which are arrays ; so you can call Java vararg functions from Kotlin and the reverse transparently
s
@CLOVIS yes, of course. makes sense 🤷‍♂️
k
> so
List
and
Array
don't have a joint interface, but both can produce an
Iterator<T>
, so you could use that as a unifying solution!?! You won't have a
for-loop
(that one works on
Iterable<T>
) Funnily enough, the
for
loop works on Iterators too, even though the docs say it needs an Iterable: https://pl.kotl.in/3f7a7Dh0_
Copy code
fun main() {
    for (c in arrayOf('a', 'b', 'c').iterator())
    println(c)
}
Ah, that's because Iterator provides an
iterator()
function!
y
which, fun fact, literally just returns itself