Eliminating the usage of exceptions where possible (like accessing an index in a list)


#1

Right now the following code:

val q = listOf("potato", "tomato")[3]

will throw an exception (index out of bounds).

The fix for this, in Kotlin, is this:

val q = listOf("potato", "tomato").getOrNull(3)

What’s the reasoning behind this verbosity? There are other languages (Elm for example) that default to the optional behavior for index retrievals.

I think it’s best to move away from exceptions where possible. (compile-time safety > runtime errors)

I feel like the first code snippet should return a String? and then if you want an exception to be thrown in the case of a failure, you’d have to explicitly call to that:

val q = listOf("potato", "tomato").getOrException(3)

I think applying this null > exceptions universally will result in fewer runtime failures.

In a similar vein, I think it’d be cool to be able to run Kotlin with a config option that turns all Any! types to Any?, so when utilizing Java libraries we don’t have to worry about NPEs. I understand the reasoning behind this (to get people to move to Kotlin from Java without having to implement safety that they didn’t have before), but I think that decision should only be temporary.


#2

Changing the function would mean a breaking change and will probably not going to happen.

Second, I agree.


#3

That’s a fair point. Maybe on a major release they’ll consider making non-backwards compatible changes? (2.0 etc.)


#4

Let’s take the Map.get(K): V? operation as an example. It returns null when the value is not in the map — so it behaves exactly the same as you propose the operation List.get should behave.

This nullable return type proved to be inconvenient in the following situations:

  • when you want to perform an augmented assignment operation on a map value:
    val map: MutableMap<Key, List<Value>> = ...
    map[someKey] += anotherValue
    
  • when you want to navigate through nested structures:
    val map: Map<Key1, Map<Key2, Value>> = ...
    val value = map[key1][key2]
    

Both of the above examples do not compile because of the nullable return type of Map.get. Thus it is questionable whether the proposed change will improve code in the common use cases.

Another possible fix is to avoid querying the list with an index out of its bounds and still use the operator [] .


#5

Those are good points I hadn’t considered.

Do you think there are other ways we could achieve the conciseness of the above while still having compile-time safety for these exception-prone operations?

The thing about the following code:

val map = mutableMapOf<String, List<String>>()
map["potato"] = (map["potato"] ?: listOf()) + "newItem"

vs the following code with potential to throw:

val map = mutableMapOf<String, List<String>>()
map["potato"]  += "newItem"

is the former is safer, and defines default functionality. I agree it’s a bit too verbose, and the only solution I can think of is simply a different flow (inspired by getOrPut):

val map = mutableMapOf<String, List<String>>()
map.appendOrPut("potato", "newItem", listOf())

appendOrPut being:

fun <T, Q> MutableMap<T, List<Q>>.appendOrPut(key: T, value: Q, default: List<Q>) {
    this[key] = (this[key] ?: default) + value
}

Yes, this is doable if you know the bounds of the list and trust the programmer doesn’t make a mistake in figuring this out. I believe that this type of job (validating bounds) would be better suited for a compiler, though.

In a world where li[key] returns T?, we could just smart cast in situations where the compiler can verify the length of a list.
This could play into another post I have about refinement types; we could expect a parameter of type

li: List<String> :: { it.size > 4 && it.size < 12 }

and then:
li[2] would return null,
li[7] would return String,
li[16] would return null

but even without refinement types, you could still do local smart casting if the list is not mutable.