David Kubecka
11/18/2023, 9:10 PMbram
11/18/2023, 9:19 PMList<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...Vampire
11/18/2023, 9:56 PMnull
) is a valid answer.bram
11/18/2023, 10:03 PMShawn
11/18/2023, 10:51 PM[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 blurryE
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.CLOVIS
11/19/2023, 10:57 AMList.getOrNull
if you don't want an exception.David Kubecka
11/19/2023, 11:27 AMCLOVIS
11/19/2023, 12:17 PMVampire
11/19/2023, 12:17 PMthe 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. :-)David Kubecka
11/20/2023, 9:27 AMa list could get new elements while an array cannotThis 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 theThis 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 🙂function to get the behavior you prefer. 🙂getOrNull
CLOVIS
11/20/2023, 9:48 AMn
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.Klitos Kyriacou
11/20/2023, 9:49 AMgetValue
to access a Map element that should exist. So there is already a List-Map equivalence:
List Map
get getValue
getOrNull get
It's just that the defaults
are different between these two.Vampire
11/20/2023, 10:11 AMCLOVIS
11/20/2023, 10:12 AMDavid Kubecka
11/20/2023, 3:33 PMA 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
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.CLOVIS
11/20/2023, 3:48 PMYou sayYes. 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.Then I think you can as well say that "a map has keys given byA list of size n has n elements indexed 0 until n .
".map.keys
David Kubecka
11/20/2023, 4:02 PMfor (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.Klitos Kyriacou
11/20/2023, 4:09 PM[]
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.Vampire
11/20/2023, 5:00 PM