https://kotlinlang.org logo
#getting-started
Title
# getting-started
d

David Kubecka

11/18/2023, 9:10 PM
Is there some intuition behind the fact that accessing a key in an empty Map yields null while accessing an index in an empty list throws an exception?
b

bram

11/18/2023, 9:19 PM
Historic reasons perhaps? Lists are often backed in memory by a contiguous block of physical memory, so seems natural to just crash and burn if someone tries reading memory outside of this block. A map on the other hand is implemented however, with pointers to anywhere in memory stored in hashbins or a tree struct or ... , so there it makes sense that the first Q is "does this key exist or not, and if not that's ok"
👍 1
A fair question though as you can transform any
List<T>
trivially into a
Map<Int, T>
where the map keys corresponds to the elements' indices. In which case it is a bit odd that one would throw and the other would return null...
v

Vampire

11/18/2023, 9:56 PM
Imho it is neither odd, nor historical, nor due to implementation detail. It is simply different semantics. A list is a list of X items, if you try to get the X+Yst element that is just out of range and worth an exception. A map is a mapping from one thing to another and if you ask what something is mapped to, nothing (i.e.
null
) is a valid answer.
10
b

bram

11/18/2023, 10:03 PM
I like this answer also ^^^^ Though still semantically I feel one can treat a list as a mapping also from a thing to an index, and then asking for an index without anything mapped to could easily return null instead of erroring.
s

Shawn

11/18/2023, 10:51 PM
I think it kinda makes more sense from a set theory perspective—lists contain a sequence of elements, and they don't have to be unique.
[1, 2, 4, 1]
is a valid
List<Int>
with a countable and finite number of elements (i.e. you can objectively say which element is the Nth element, within the bounds of the list length). Maps don't intrinsically have order, and their key set is only bound by the data type you're using to key the map (and also you can't have duplicates). In a list of 5 elements, you can confidently say that there will be an element at position 2, and you can say that there can't be an element at position 64 because the list isn't that long. In a
Map<String, String>
, for example, there are no such guarantees. The set of possible keys is uncountably infinite, and there's no information about the domain of the keyset you can confidently infer by being aware of any subset of that keyset. If you have a map that looks like this:
{"foo": "bar", "baz": "qux"}
and you only know that "foo" is a key in the map, there's nothing you can infer from that to prove that "baz" is a key without having to check for yourself. "No such mapping" is always a possible answer when looking up keys in a map, so it makes sense to return
V?
by default. It's only when you start using the map in other ways through implementation that the distinction from other data structures becomes blurry
Thinking about it in terms of "oh you could maybe think about a List as a special case of Map where the keys are numbers", that's sorta true, but it goes much deeper than that. If you want to use a Map like a List, you would have to have a magical kind of Map that only maps its values to a keyset that is countable and ordered. That means the keyset consists of elements that can be related to each other. The set doesn't have to be finite, but it does need an origin (i.e. a minimum value or a beginning to the set). The map also can't have holes. That is to say, if you have a map keyed with elements from a set
E
and you have values mapped to
E[n]
and
E[n + 2]
, it is invalid to not have any mapping for
E[n + 1]
. This also means that you can apply a function to a given element of the set and get a valid predecessor or successor as a result, within the boundaries of the list. The easiest way to use a Map like a List would be to constrain the domain of its keys to a contiguous subset of the natural numbers (ℕ) starting from 0 (you can't have a
–1
th element, for example). The map must also disallow arbitrary keying of the map when appending mappings. If you have a three-element list, you cannot add a fifth element without first adding a fourth. tl;dr, if you were to treat a List like a collection of mappings, it would be valid to say that the domain of those mappings is way more constrained and easier to reason about its contents compared to the domain of a Map. How people typically use Lists (to order things and iterate over them in sequence, with a clear start and end) makes it far less likely to run into the notion of "no mapping" through normal usage than you would with a Map, and therefore throwing an exception when you go out of bounds makes more sense than constantly having to null-check when getting elements out of a List. if the tl;dr was too long, remember that it's actually pretty uncommon/usually unnecessary to access list elements by index (unless you're treating the list like a tuple), and that it's actually impossible to access Map values without getting by index, until you perform some operation that lets you treat the map more like a list. The usage is different, and therefore the ergonomics should be, too.
c

CLOVIS

11/19/2023, 10:57 AM
Note that there is
List.getOrNull
if you don't want an exception.
d

David Kubecka

11/19/2023, 11:27 AM
I'm not entirely convinced by the apparently most popular answer by @Vampire. Namely, the statement "A list is a list of X items" seems to me to describe an Array semantics with all its memory storage connotations. However, I tend to view both List and Map as abstract collections with no particular assumptions about their concrete implementations. You could certainly make any number of "philosophical" arguments in favor of the current behaviour but I still think that the more natural way would be for these cases to behave consistently as e.g. in Python.
c

CLOVIS

11/19/2023, 12:17 PM
Well, lists are sequentially indexed. You can't have a sparse list. That's the main difference.
v

Vampire

11/19/2023, 12:17 PM
the statement "A list is a list of X items" seems to me to describe an Array semantics
Sure, list and array are exactly the same. They are logically both a list of X items. The implementation of this list is different, and a list could get new elements while an array cannot, and so on. But ultimately they are both a list of things.
with all its memory storage connotations.
No, that is then part of the detail differences between a list and an array. But only eventually. An
ArrayList
for example stores it's elements in an array so indeed have the same memory behaviour. A
LinkedList
does not. That's all implementation detail, but all are a list of X things.
However, I tend to view both List and Map as abstract collections with no particular assumptions about their concrete implementations.
Me too. :-)
but I still think that the more natural way would be for these cases to behave consistently as e.g. in Python.
Well, you are welcome to suggest to JetBrains to change this, but I highly doubt you will find many supporters for this change. :-D In the meantime just use the
getOrNull
function to get the behavior you prefer. :-)
1
d

David Kubecka

11/20/2023, 9:27 AM
a list could get new elements while an array cannot
This is for me the key difference. It seems strange that something that possibly can have an element at index 0 in the future throws an exception only because now it is empty. In other words, "index out of bounds" is fully understandable in case of fixed size Array but less so in case of (Mutable)List.
Well, you are welcome to suggest to JetBrains to change this, but I
highly doubt you will find many supporters for this change. :-D In the
meantime just use the
getOrNull
function to get the behavior you prefer. 🙂
This is really not about me preferring any particular variant. I just thought that there was an inconsistency between map/list behaviour. Namely, I think that the same argument for list throwing an exception can be made for map as well. Anyway, the main goal of my question (understanding the reasoning behind the design decision) was fulfilled so thank you for your answers 🙂
👌 1
c

CLOVIS

11/20/2023, 9:48 AM
A list of size >0 can never not have an element at index 0. That's the definition of the list's size. A list of size
n
has
n
elements indexed
0 until n
. Everything in that range is legal, and returns whatever element is stored. Everything outside that range is an IndexOutOfBoundsException because it is not defined. Lists are not allowed to be sparse (have "holes" in them). Maps are arbitrary mappings, they are allowed to be sparse if you want them to. Therefore, you cannot know in advance if an element is present or not.
1
k

Klitos Kyriacou

11/20/2023, 9:49 AM
I often use
getValue
to access a Map element that should exist. So there is already a List-Map equivalence:
Copy code
List       Map
get        getValue
getOrNull  get
It's just that the
defaults
are different between these two.
v

Vampire

11/20/2023, 10:11 AM
@CLOVIS maybe change "now" to "not" to prevent confusion. 😉
c

CLOVIS

11/20/2023, 10:12 AM
Ouch, yes. Sorry
👌 1
d

David Kubecka

11/20/2023, 3:33 PM
@CLOVIS You say
A list of size n has n elements indexed 0 until n .
Then I think you can as well say that "a map has keys given by
map.keys
". That is, in both cases you can ensure in advance that you access only valid indexes, resp. keys. On the other hand, given just
Copy code
fun foo(map: Map, list: List)
you don't have any prior knowledge about the structure of either collection. In this context it seems inconsistent (and that's my whole point) that one collection throws an exception while the one other just returns null.
c

CLOVIS

11/20/2023, 3:48 PM
You say
A list of size n has n elements indexed 0 until n .
Then I think you can as well say that "a map has keys given by
map.keys
".
Yes. The difference is one can be sparse, the other cannot. So on one side you're likely to find nothing (and that's normal), on the other side if you find nothing it must be a programming error.
d

David Kubecka

11/20/2023, 4:02 PM
I don't find this sparseness argument convincing TBH. That's probably because I'm more inclined to look at the similarities between both collection types rather than at their differences. Namely, I see that both collections can be iterated in exactly the same way via
Copy code
for (elem in collection) {
  collection[elem]
}
Under the hood the compiler of course generates different code for each collection type, but the point is that once you have obtained the sequence of elements then you really don't care about the structure of that sequence. But of course that's just my personal take on the topic and I'm probably in a minority here which is totally okay.
k

Klitos Kyriacou

11/20/2023, 4:09 PM
Kotlin offers both the null-returning and exception-throwing variants of method for both List and Map. It's just a question of which one of those variants is the one that uses the "natural-looking"
[]
syntax. Looking at other languages, some use the exception-throwing variant (Python, Ada, C#) while others use the null- or default-value-returning variant (Java, Kotlin, C++, Rust). There's no right or wrong way; just convention for a specific language.
v

Vampire

11/20/2023, 5:00 PM
I suggest we just stop here. 🙂 David made his point that from his PoV it is inconsistent and that's fine. He also said, that we sufficiently explained to him why it is like it is, so that's also fine. Any further discussion imho is just trying to convince the other party from the own PoV which will most likely not happen at this stage. 🙂
1
👍 1