4

As it was in Java, any object that is Iterable can be used in an enhanced for loop. I thought the same held true for Kotlin as well, until I discovered that kotlin.collections.Map is not Iterable at all.

from the standard library source code:

public interface Map<K, out V> {
    // ....

If not, then how is something like this possible?:

val map: Map<String, Int> = HashMap()
// ...
for ((s, i) in map) {
    println(s)
    println(i)
    // works perfectly
}

Finally, is it possibly to make custom classes that support this syntax? If so, how?

SMMH
  • 310
  • 1
  • 13
  • 3
    `has a member or an extension function iterator() that returns Iterator<>`: https://kotlinlang.org/docs/control-flow.html#for-loops – awesoon Jun 04 '22 at 19:09
  • https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/iterator.html – lukas.j Jun 04 '22 at 19:14
  • And the code: https://github.com/JetBrains/kotlin/blob/6a670dc5f38fc73eb01d754d8f7c158ae0176ceb/libraries/stdlib/src/kotlin/collections/Maps.kt#L373 – lukas.j Jun 04 '22 at 19:14
  • 1
    `Map.entries()` is Iterable. Same as in Java, Map is not itself iterable. – Tenfour04 Jun 04 '22 at 19:44

1 Answers1

6

The Kotlin for loop is defined by convention:

Some syntax forms in Kotlin are defined by convention, meaning that their semantics are defined through syntactic expansion of one syntax form into another syntax form.

In the case of for(VarDecl in C) Body, the syntax form that it expands to is:

when(val $iterator = C.iterator()) {
    else -> while ($iterator.hasNext()) {
                val VarDecl = $iterator.next()
                <... all the statements from Body>
            }

as specified here in the spec.

Rather than requiring a particular interface, it is only required that overload resolution finds appropriate operator overloads for iterator(), hasNext() and next(). As an extreme example, this compiles!

fun main() {
    for (s in Foo()) { // no error here!
        
    }
}

class Foo {
    operator fun iterator(): Bar = Bar()
}

class Bar {
    operator fun hasNext() = false
    operator fun next() = ""
}

In your case, there exists an extension function iterator for Map:

operator fun <K, V> Map<out K, V>.iterator(): Iterator<Entry<K, V>>

and MutableMap:

@JvmName("mutableIterator") 
operator fun <K, V> MutableMap<K, V>.iterator(): MutableIterator<MutableEntry<K, V>>

The iterator method don't have to be declared in Map - as long as overload resolution can resolve it, the for loop works. The same applies to next and hasNext.

So to make your own class work with for loops, the minimum you need is to declare:

operator fun iterator(): Iterator<Something>

But I would still suggest that you implement Iterable, because then you get all the Iterable extension functions for free :)

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • I understand that extension functions are awesome and they allow Map to act like it's Iterable, but since those extension functions are a part of the standard library anyway, why not just make Map Iterable? and have those extension functions as member functions. – SMMH Jun 05 '22 at 16:50
  • 1
    @SMMH I cannot give you a definitive answer for why `Map` is not *designed* to implement `Iterable` because I did not design it, but if I had to guess, it is because a map can be viewed from many perspectives - you can see it as a collection of keys, a collection values, or a collection of entries. It can't implement all of those interfaces at once because of type erasure, so this is what we've got. See also: https://stackoverflow.com/questions/19422365/reason-hashmap-does-not-implement-iterable-interface – Sweeper Jun 05 '22 at 16:57
  • 1
    It can't be `Iterable` because on JVM it's mapped to `java.util.Map` which doesn't extend `java.lang.Iterable`. I don't know if that's the only reason, but it's a sufficient one. – Alexey Romanov Jun 10 '22 at 07:11
  • 1
    @AlexeyRomanov but then that just raises the question of “why doesn’t JVM’s `Map` implement `Iterable`”, which is why I gave a link with possible answers to *that* instead. – Sweeper Jun 10 '22 at 07:20
  • But you should be clear it's a different question. – Alexey Romanov Jun 10 '22 at 07:48
  • @AlexeyRomanov You’re right. I admit that I could have made it clearer. – Sweeper Jun 10 '22 at 07:53