Map key-not-found (null vs exception)

3,975 views
Skip to first unread message

Olov Lassus

unread,
Dec 2, 2011, 10:14:11 AM12/2/11
to mi...@dartlang.org
Map[key] returns null if key is not in the map. It could instead throw an exception, consistent with collections such as List and Queue. The upside would be possibly more robust code (assuming that not all map[key] call-sites check for null), the downside.. worse compiled-to-JavaScript performance perhaps? Are there other downsides?

What are your thoughts on this? Perhaps you've considered (and rejected) it already while designing Map?

It could go hand in hand with a V getOrElse(K key, V defaultValue) Map method. Which would be handy for other reasons as well, counting occurrences being the obvious example.

It would also get rid of the null value/not-found ambiguity and it plays well with Bob's Null-safety proposal. My prime interest is robustness, however.

Looking at a few other languages, Java returns null while .NET/C#'s generic Dictionary throws. Python's dict throws too.

/Olov

Charles Forsyth

unread,
Dec 2, 2011, 10:48:06 AM12/2/11
to Olov Lassus, mi...@dartlang.org
On 2 December 2011 15:14, Olov Lassus <olov....@gmail.com> wrote:>
Looking at a few other languages, Java returns null while .NET/C#'s
generic Dictionary throws. Python's dict throws too.
Joe Armstrong discusses a more subtle approach to library design,
including the use of exceptions, in his thesis
(http://www.erlang.org/download/armstrong_thesis_2003.pdf); see
section 4.5 ("Intensional programming").
He gives the example of an interface to dict, which makes it
particularly relevant here.
Originally, the interface had lookup(Key, Dict) -> {ok, Value} | notfound.
It was used in (at least) three different ways: to test whether a key
was in a dictionary; to find the value of a key that might be in
the dictionary; and to fetch the value of a key that MUST be in the
dictionary. The revised interface to dict expressed the
three different actions as distinct operations, to make the
programmer's intensions clear, without detailed analysis of the code:
dict:fetch(Key, Dict) = Val | EXIT
dict:search(Key, Dict) = {found, Val} | not_found.
dict:is_key(Key, Dict) = Boolean
The first two are of interest here: search is used when a value might
or might not be in the dictionary, and returns an
atom (used in pattern matching) to distinguish the cases; by contrast,
fetch is used when the programmer asserts that
the value must be in the dictionary, or there's a programming error,
provoking an exception (the EXIT).
He notes that fetch can be implemented in terms of search, but the
other way round is ill-advised (that's part of the subtlety).
Exceptions should be reserved for actual errors, not routinely caused
then corrected. Certainly one of the painful things
about Java (and perhaps C#) is the extent to which code ends up
cluttered with "exception" handlers that handle things that
aren't, in fact, exceptions, but a common case. Of course, I can and
do encapsulate the exception handlers in those cases as
separate functions that catch the particular exception and return what
I like (ie, Armstrong's ill-advised method). Imagine, though,
trying to trace an errant program; if exceptions are heavily used by
the libraries and run-time system for cases
that are not truly exceptional, then a given exception might or might
not warrant investigation. By contrast, in Armstrong's
structure, you won't include exception handlers for dict operations at
all. Instead, the process will fail if it violates the implied
assertion that (for instance) a given key must be present in the
dictionary.

One reasons that exceptions got a bad name was that they have been
overused, or mainly used, to express unexceptional conditions.

Olov Lassus

unread,
Dec 2, 2011, 11:18:32 AM12/2/11
to Charles Forsyth, mi...@dartlang.org
That's to my point, isn't it?

A throwing Map.operator[] would differ from the null-returning
Map.operator[] by imposing a precondition (key must exist in map) and
would throw only if that precondition is broken, in the same manner
that List.operator[] throws when the index is out of bounds or
Queue.first throws when it's empty. That doesn't mean that the user
should test for key availability using try-catch and operator[].

dict:fetch(Key, Dict) = Val | EXIT

corresponds to V (or thrown exception) Map.operator[](K key)

dict:search(Key, Dict) = {found, Val} | not_found

corresponds to a special case of V getOrElse(K key, V defaultValue)

dict:is_key(Key, Dict) = Boolean

corresponds to Map.containsKey(K key) = bool

/Olov

Charles Forsyth

unread,
Dec 2, 2011, 11:31:40 AM12/2/11
to Olov Lassus, mi...@dartlang.org
On 2 December 2011 16:18, Olov Lassus <olov....@gmail.com> wrote:
> That's to my point, isn't it?

yes, sorry. by "more subtle approach" I meant "more subtle approach
than usual", not specifically your proposal,
which did include those distinct operations. it was prompted mainly by
your closing remark about Java, C#, Python, etc.
think of it as supporting your proposal and suggesting that there
should perhaps be fewer unexceptional exceptions in other library
interfaces.

Bob Nystrom

unread,
Dec 2, 2011, 1:57:05 PM12/2/11
to Olov Lassus, mi...@dartlang.org
On Fri, Dec 2, 2011 at 7:14 AM, Olov Lassus <olov....@gmail.com> wrote:
Map[key] returns null if key is not in the map. It could instead throw an exception, consistent with collections such as List and Queue. The upside would be possibly more robust code (assuming that not all map[key] call-sites check for null), the downside.. worse compiled-to-JavaScript performance perhaps? Are there other downsides?

Try/catch blocks are pretty syntactically heavy in Dart, so if we did that, I suspect that people would just do:

if (map.contains('key')) {
  var value = map['key'];
}

and then you're wasting time doing the lookup twice. C# handles this by having TryGetValue() which is a bit cumbersome but works. We can be a bit more flexible in Dart since it's dynamically-typed: any variable can hold a value of any type, unlike in C# where you could have a Dictionary of non-reference value type that doesn't allow null.


It would also get rid of the null value/not-found ambiguity and it plays well with Bob's Null-safety proposal. My prime interest is robustness, however.

I'm not totally sold on map[] returning null when a key isn't present (for one thing, it makes it trickier to tell if you have a present key whose value is null), but my nullability proposal was designed with this case in mind. With it, the return type of operator[] in Map would be T?, not T, so it's at least clear to callers that it may not return a T.


Looking at a few other languages, Java returns null while .NET/C#'s generic Dictionary throws. Python's dict throws too.

I believe Dictionary throws in C# mainly because you can instantiate generics with value types that aren't nullable. Java doesn't support primitive type arguments which is why it can get away with returning null.

- bob

Olov Lassus

unread,
Dec 2, 2011, 3:03:22 PM12/2/11
to Bob Nystrom, mi...@dartlang.org
On Fri, Dec 2, 2011 at 7:57 PM, Bob Nystrom <rnys...@google.com> wrote:
> Try/catch blocks are pretty syntactically heavy in Dart, so if we did that,
> I suspect that people would just do:
>
> if (map.contains('key')) {
>   var value = map['key'];
> }
>
> and then you're wasting time doing the lookup twice. C# handles this by
> having TryGetValue() which is a bit cumbersome but works. We can be a bit
> more flexible in Dart since it's dynamically-typed: any variable can hold a
> value of any type, unlike in C# where you could have a Dictionary of
> non-reference value type that doesn't allow null.

Valid point, though getOrElse would be preferred over that anti-pattern:
var value = map.getOrElse('key', null);

...which on the flip-side can take any default-value (not just null)
but on the flop-side is a bit more verbose than a null-returning
operator[]. But perhaps many would still use contains because it's
more familiar.

> I'm not totally sold on map[] returning null when a key isn't present (for
> one thing, it makes it trickier to tell if you have a present key whose
> value is null), but my nullability proposal was designed with this case in
> mind. With it, the return type of operator[] in Map would be T?, not T, so
> it's at least clear to callers that it may not return a T.

True. And it would need to be T? even in the case of a throwing
operator[] since the Map can contain null. I stand corrected. :)

> I believe Dictionary throws in C# mainly because you can instantiate
> generics with value types that aren't nullable. Java doesn't support
> primitive type arguments which is why it can get away with returning null.

That seems to be the reason since the pre-generic Hashtable indeed returns null.

I have a better understanding of the trade-off already - thanks!

/Olov

John

unread,
Dec 2, 2011, 4:26:31 PM12/2/11
to General Dart Discussion
I like how .net Linq handles this (if you assume a Map[] or
Dictionary[] is really just a query for a single result)

if I want to throw on a non-exist (or if more than one exists, in this
case)

myIEnumerableQuery.Single(); //throws if does not return exactly 1
result, otherwise returns result

If I don't want to throw:

myIEnumerableQuery.SingleOrDefault(); //returns the default(Type) for
that type, usually null, otherwise returns result


Just a thought.

On Dec 2, 2:03 pm, Olov Lassus <olov.las...@gmail.com> wrote:

Bob Nystrom

unread,
Dec 2, 2011, 4:35:42 PM12/2/11
to Olov Lassus, mi...@dartlang.org
On Fri, Dec 2, 2011 at 12:03 PM, Olov Lassus <olov....@gmail.com> wrote:

Valid point, though getOrElse would be preferred over that anti-pattern:
var value = map.getOrElse('key', null);

...which on the flip-side can take any default-value (not just null)
but on the flop-side is a bit more verbose than a null-returning
operator[].

Right. The above works for some cases, but doesn't work if you only want to create the default value when needed. Imagine:

var value = map.getOrElse('key', new List(999999));

You really don't want to create the default every time just do discard it. You could do:

var value = map.getOrElse('key', () => new List(999999));

but that starts to feel a little too clever for my taste. The challenge here is that you really want both getting-a-value and flow-control-based-on-key-presence in one compact operation. Without pattern-matching, it's hard to do that well.

- bob

Olov Lassus

unread,
Dec 2, 2011, 5:44:03 PM12/2/11
to Bob Nystrom, mi...@dartlang.org
On Fri, Dec 2, 2011 at 10:35 PM, Bob Nystrom <rnys...@google.com> wrote:
> Right. The above works for some cases, but doesn't work if you only want to
> create the default value when needed. Imagine:
>
> var value = map.getOrElse('key', new List(999999));
>
> You really don't want to create the default every time just do discard it.
> You could do:
>
> var value = map.getOrElse('key', () => new List(999999));
>
> but that starts to feel a little too clever for my taste. The challenge here
> is that you really want both getting-a-value and
> flow-control-based-on-key-presence in one compact operation. Without
> pattern-matching, it's hard to do that well.

I personally prefer the simpler getOrElse that takes a value (not a
function). The more complicated getOrElse becomes, the bigger risk for
the containsKey-then-[] anti-pattern. Complex default-values can still
be handled the same as today with the current null-returning behavior.

var value = map.getOrElse('key', null); // instead of: var value = map['key'];
if (value == null) {
value = new List(999999);
}

But V getOrElse(K key, V default()) has it's merits too. It's similar
to putIfAbsent, if nothing else.

/Olov

Colin Putney

unread,
Dec 3, 2011, 2:04:02 PM12/3/11
to mi...@dartlang.org
On Fri, Dec 2, 2011 at 1:35 PM, Bob Nystrom <rnys...@google.com> wrote:

> You really don't want to create the default every time just do discard it.
> You could do:
>
> var value = map.getOrElse('key', () => new List(999999));
>
> but that starts to feel a little too clever for my taste. The challenge here
> is that you really want both getting-a-value and
> flow-control-based-on-key-presence in one compact operation. Without
> pattern-matching, it's hard to do that well.

Could you elaborate on this a bit more? This is quite a good solution.
What are your reasons for rejecting it? Is "too clever" code for "too
much like Smalltalk to be acceptable to mainstream programmers?"

Colin

Alan Knight

unread,
Dec 3, 2011, 2:43:46 PM12/3/11
to Colin Putney, mi...@dartlang.org
And if you did it as
   get('key', orElse: ()=> new List(999999))
with an optional keyword argument, it'd be way too much like Smalltalk :-)

For extra excessive cleverness points, it could take either a value or a function and do the appropriate thing (but that breaks down if the value you want returned is a function).

It also won't work as is because "get" is a reserved word, so you'd need to come up with a different name.



Colin Putney
3 December, 2011 2:04 PM



Could you elaborate on this a bit more? This is quite a good solution.
What are your reasons for rejecting it? Is "too clever" code for "too
much like Smalltalk to be acceptable to mainstream programmers?"

Colin


Bob Nystrom
2 December, 2011 4:35 PM





Valid point, though getOrElse would be preferred over that anti-pattern:
var value = map.getOrElse('key', null);

...which on the flip-side can take any default-value (not just null)
but on the flop-side is a bit more verbose than a null-returning
operator[].

Right. The above works for some cases, but doesn't work if you only want to create the default value when needed. Imagine:

var value = map.getOrElse('key', new List(999999));

You really don't want to create the default every time just do discard it. You could do:

var value = map.getOrElse('key', () => new List(999999));

but that starts to feel a little too clever for my taste. The challenge here is that you really want both getting-a-value and flow-control-based-on-key-presence in one compact operation. Without pattern-matching, it's hard to do that well.

- bob



Olov Lassus
2 December, 2011 3:03 PM


On Fri, Dec 2, 2011 at 7:57 PM, Bob Nystrom <rnys...@google.com> wrote:
Try/catch blocks are pretty syntactically heavy in Dart, so if we did that,
I suspect that people would just do:

if (map.contains('key')) {
  var value = map['key'];
}

and then you're wasting time doing the lookup twice. C# handles this by
having TryGetValue() which is a bit cumbersome but works. We can be a bit
more flexible in Dart since it's dynamically-typed: any variable can hold a
value of any type, unlike in C# where you could have a Dictionary of
non-reference value type that doesn't allow null.
Valid point, though getOrElse would be preferred over that anti-pattern:
var value = map.getOrElse('key', null);

...which on the flip-side can take any default-value (not just null)
but on the flop-side is a bit more verbose than a null-returning
operator[]. But perhaps many would still use contains because it's
more familiar.

I'm not totally sold on map[] returning null when a key isn't present (for
one thing, it makes it trickier to tell if you have a present key whose
value is null), but my nullability proposal was designed with this case in
mind. With it, the return type of operator[] in Map would be T?, not T, so
it's at least clear to callers that it may not return a T.
True. And it would need to be T? even in the case of a throwing
operator[] since the Map can contain null. I stand corrected. :)

I believe Dictionary throws in C# mainly because you can instantiate
generics with value types that aren't nullable. Java doesn't support
primitive type arguments which is why it can get away with returning null.
That seems to be the reason since the pre-generic Hashtable indeed returns null.

I have a better understanding of the trade-off already - thanks!

/Olov


Bob Nystrom
2 December, 2011 1:57 PM




On Fri, Dec 2, 2011 at 7:14 AM, Olov Lassus <olov....@gmail.com> wrote:
Map[key] returns null if key is not in the map. It could instead throw an exception, consistent with collections such as List and Queue. The upside would be possibly more robust code (assuming that not all map[key] call-sites check for null), the downside.. worse compiled-to-JavaScript performance perhaps? Are there other downsides?

Try/catch blocks are pretty syntactically heavy in Dart, so if we did that, I suspect that people would just do:

if (map.contains('key')) {
  var value = map['key'];
}

and then you're wasting time doing the lookup twice. C# handles this by having TryGetValue() which is a bit cumbersome but works. We can be a bit more flexible in Dart since it's dynamically-typed: any variable can hold a value of any type, unlike in C# where you could have a Dictionary of non-reference value type that doesn't allow null.


It would also get rid of the null value/not-found ambiguity and it plays well with Bob's Null-safety proposal. My prime interest is robustness, however.

I'm not totally sold on map[] returning null when a key isn't present (for one thing, it makes it trickier to tell if you have a present key whose value is null), but my nullability proposal was designed with this case in mind. With it, the return type of operator[] in Map would be T?, not T, so it's at least clear to callers that it may not return a T.

Looking at a few other languages, Java returns null while .NET/C#'s generic Dictionary throws. Python's dict throws too.
I believe Dictionary throws in C# mainly because you can instantiate generics with value types that aren't nullable. Java doesn't support primitive type arguments which is why it can get away with returning null.

- bob


Olov Lassus
2 December, 2011 10:14 AM


Map[key] returns null if key is not in the map. It could instead throw an exception, consistent with collections such as List and Queue. The upside would be possibly more robust code (assuming that not all map[key] call-sites check for null), the downside.. worse compiled-to-JavaScript performance perhaps? Are there other downsides?

What are your thoughts on this? Perhaps you've considered (and rejected) it already while designing Map?

It could go hand in hand with a V getOrElse(K key, V defaultValue) Map method. Which would be handy for other reasons as well, counting occurrences being the obvious example.

It would also get rid of the null value/not-found ambiguity and it plays well with Bob's Null-safety proposal. My prime interest is robustness, however.

Looking at a few other languages, Java returns null while .NET/C#'s generic Dictionary throws. Python's dict throws too.

/Olov

Colin Putney

unread,
Dec 3, 2011, 3:44:25 PM12/3/11
to kni...@acm.org, mi...@dartlang.org
On Sat, Dec 3, 2011 at 11:43 AM, Alan Knight <kni...@acm.org> wrote:
> And if you did it as
>    get('key', orElse: ()=> new List(999999))
> with an optional keyword argument, it'd be way too much like Smalltalk :-)
>
> For extra excessive cleverness points, it could take either a value or a
> function and do the appropriate thing (but that breaks down if the value you
> want returned is a function).
>
> It also won't work as is because "get" is a reserved word, so you'd need to
> come up with a different name.

Hmm. What would be a good name... It should be something that uses the
vocabulary of maps, something that suggests location. And the keyword
should suggest the lack of something in that location, its absence.
How about something like:

map.at('key', ifAbsent: () => new List(999999))

There, that reads pretty well. :-)

Ok, humour aside, I *was* trying to avoid the all-too-common "Hey,
Dart should be more like my favourite language" post. That's just not
constructive. Dart is not Smalltalk any more than it is Javascript or
Java or C#. We obviously want to develop idioms that serve the Dart
community well, regardless of what languages they've used in the past
or what they're doing with Dart today.

Nevertheless, Dart does have a lot of features that are similar to
those of Smalltalk. It seems reasonable to adopt some of the library
patterns that have grown up around those features and stood up to a
few decades of use. Using closures rather than exceptions to handle
common or expected errors is a good candidate, I think. It solves all
the issues brought up in this thread, flexibly and succinctly.
Dismissing it as "too clever" without any further discussion doesn't
seem constructive to me.

Colin

Gilad Bracha

unread,
Dec 3, 2011, 4:44:42 PM12/3/11
to Colin Putney, kni...@acm.org, mi...@dartlang.org
Hi Colin,

I expect the libs to get a serious overhaul in the next few months. It'll largely be up the people doing that work to decide what patterns to use. I'll make sure the person doing collections knows about these idioms. 

One thing to bear in mind is that Dart does not support non-local returns :-(.  This was not a decision taken lightly. It's just hard to implement it efficiently on top of Javascript. That does mean that some tried and true Smalltalk idioms won't work as well in Dart.
--
Cheers, Gilad

Colin Putney

unread,
Dec 5, 2011, 1:09:43 PM12/5/11
to Gilad Bracha, mi...@dartlang.org
On Sat, Dec 3, 2011 at 1:44 PM, Gilad Bracha <gbr...@google.com> wrote:
> Hi Colin,
>
> I expect the libs to get a serious overhaul in the next few months. It'll
> largely be up the people doing that work to decide what patterns to use.
> I'll make sure the person doing collections knows about these idioms.

Excellent! I find that library quality is at least as important as
language semantics in making a pleasant programming environment, so
I'm very glad they'll be getting some love.

> One thing to bear in mind is that Dart does not support non-local returns
> :-(.  This was not a decision taken lightly. It's just hard to implement it
> efficiently on top of Javascript. That does mean that some tried and true
> Smalltalk idioms won't work as well in Dart.

Yeah, that's probably the right decision at this stage. :-(
Fortunately, non-local returns are the kind of thing that could be
added later, if and when circumstances change.

Cheers,

Colin

Reply all
Reply to author
Forward
0 new messages