Maps and hashes in Dart

(This is Part 4 of our ongoing series about Dart. Check out Part 3: Lists and arrays in Dart.)

Warning: We expect the Dart libraries to undergo potentially sweeping changes before Dart goes to alpha. This document is relevant as of 2012-01-02.

Intro

Dart is not just a language, but a full "batteries included" effort to create a productive, familiar, and fun environment for the modern web app developer. The bundled libraries are one such "battery", including many common classes and utilities such as date manipulation, regular expressions, even asynchronous constructs like Future and Promise.

Probably the most used set of libraries will be the Collections, such as List, Map, and Set. In this post, we'll take a look at Map, which is a mapping of keys to values.

In Dart, a Map is an interface designed to manipulate a collection of keys which point to values. Maps can have null values, and can have parameterized types. Note, Maps do not subclass the Collection interface.

Dart != JavaScript: Unlike JavaScript, Dart objects themselves aren't maps or hashes.

Create, read, count, copy, and remove examples

Dart supports map literals, such as:

main() {
  var stuff = {"hello": "world"};
}

In the above example, hello is the key and world is the value.

Map literals, those declared with the {} syntax, must have Strings as keys.

var map = {};

// is the same as

var map = new Map<String, dynamic>();

However, if you declare your Map without the literal syntax (eg new Map()), you can use any object that implements the hashCode method as the key. Curious what makes a good hashCode? Josh Bloch covers hashCode in his book Effective Java, and luckily that particular topic is covered in the free Chapter 3.

Of course, the values don't have to be strings:

main() {
  var stuff = {"hello": 4};
}

Dart Maps can contain null values, too:

main() {
  var stuff = {"hello": null};
}

Retrieving a value from a map is familiar:

main() {
  var stuff = {"hello": "world"};
  print( stuff['hello'] ); // world
}

If you look for a key that isn't in a Map, you will get a null in return:

main() {
  var stuff = {"hello": "world"};
  print( stuff['foo'] ); // null
}

Adding a new key/value pair to an existing map is also familiar:

main() {
  var stuff = {"hello": "world"};
  stuff['Dart'] = 'Is Cool';
  print( stuff['Dart'] ); // Is Cool
}

Use the length getter to return the number of key/value pairs in the Map:

main() {
  var stuff = {"hello": "world"};
  stuff['Dart'] = 'Is Cool';
  print( stuff.length ); // 2
}

Removing a value from a map, which also removes its key, is straight forward:

main() {
  var stuff = {"hello": "world"};
  stuff['Dart'] = 'Is Cool';
  print( stuff['Dart'] ); // Is Cool
  stuff.remove('hello');
  print( stuff.length ); // 1
  print( stuff['hello'] ); // null
}

You can copy a Map with a named constructor:

main() {
  var stuff = new Map.from({"hello": "world"});
  print( stuff['hello'] ); // world
}

Iterating

There are a few ways to iterate through the contents of a Map. If you want to access both the key and value at the same time, the forEach method will work nicely.

main() {
  var stuff = {"hello": "world", 'Dart': 'Is Cool'};
  stuff.forEach((k,v) => print(k));
}

Notice how, in the above example, that both the key and the value as passed to the callback function for every iteration of forEach.

No order is implied, do not depend on a certain order of keys and values being returned via forEach.

If you are only interested in just the keys, or just the values, use getKeys() and getValues(), respectively. Both methods return a Collection object.

main() {
  var stuff = {"hello": "world", 'Dart': 'Is Cool'};
  var keys = stuff.getKeys();
  print( keys.length ); // 2
  keys.forEach((k) => print(k)); // hello, Dart
}

putIfAbsent

A fun method on Map is putIfAbsent(K key, V ifAbsent()). It will first if the key exists, and if not, will run the ifAbsent function and will use the return value as the value for the key. For example:

main() {
  var stuff = {"hello": "world", 'Dart': 'Is Cool'};
  stuff.putIfAbsent('foo', () {
    // calculate something awesome
    return 'bar';
  });
  print( stuff['foo'] ); // bar
}

Parameterized types, aka Generics

I haven't written a post on generics yet, so please bear with me if you don't know what generics are. If you do, you'll be pleasantly surprised at how they've been simplified. If you don't know what generics are, think of them as annotations to help you and your tools understand more about the types you'll use with objects such as collections.

Maps can be specified with parameterized types, which allow you to say what kind of types you want to store inside the map. For example, perhaps you want a Map of Strings to integers. Without generics, you'd have to just hope everyone (including you) puts in String/int pairs, and you'd probably have to add a bunch of assertions to ensure that they (or you) did. However, with generics, the tools and runtime can help ensure that the code maps Strings to integers.

(The examples in the post so far have not specified any parameterized types.)

In Dart, you can be more specific about your Map, and actually say "A Map that maps Strings to integers" in this way:

main() {
  var stuff = <String, int>{'four': 4, 'five': 5};
  print( stuff['four'] ); // 4
}

The above is the same as:

main() {
  var stuff = new Map<String, int>();
  stuff['four'] = 4;
  stuff['five'] = 5;
  print( stuff['four'] ); // 4
}

Our tools come to the rescue when someone (definitely not you, because you're a Dart viking) accidentally assigns the wrong type. For example, if you run the below in Checked Mode:

main() {
  var stuff = new Map<String, int>();
  stuff['four'] = "oh uh!"; // Failed type check: type String is not assignable to type int
  print( stuff['four'] ); // won't get this far
}

Now that's what I'm talking about! For the small cost of specifying the expected types at object instantiation time, the system (in checked mode) will warn you the wrong type is being used, and the program will stop running. (again, if you're in checked mode, this is true) Check out the above example and play with the "checked mode" toggle.

You can go further and even specify the parameterized types statically, such as:

main() {
  Map<String, int> stuff = new Map<String, int>();
  stuff['four'] = "oh uh!"; // warning here if NOT in checked mode
  print( stuff['four'] ); // won't get this far
}

Now the system has even more information, so you will get further warnings even if you're not in checked mode! The program will still run if you have checked mode turned off, since the above program still works. However, by specifying the static type for stuff, you've added enough annotations to give you warnings that something isn't quite right. Try the above code yourself, with and without "checked mode" enabled.

As mentioned above, if you are using the non-literal syntax to declare a Map (such as new Map()) then you can use any object that implements hashCode as the key.

Summary

Dart's bundled libraries includes a Map interface, which can map a key (as long as it implements hashCode) to a value for easy lookup. The interface has the standard methods for creating, reading, updating, and deleting key/value pairs, as well as some new methods like putIfAbsent(). Just like in JavaScript, Dart Maps can be created with a literal, terse syntax (however, this restricts you to using only Strings as keys.) Unlike JavaScript, though, Dart objects aren't themselves Maps.

Just as Dart has optional types, parameterized types are optional as well. They are quite useful when you're writing method signatures and interfaces, as they convey even more intention and documentation about the expected containers and objects. The more type annotations you can add, the more rich documentation can be extracted and the more the tools can warn you when things don't go as planned.

Next Steps

Read Part 6 on Generics in Dart, or Why JavaScript developers should care about types.

The core Dart libraries will almost certainly undergo sweeping changes now that Josh Bloch has joined the team. For now, though, there's still a lot you can do with Dart's lists. As you work with the libraries, remember that Dart is in Technology Preview mode, and we really want to hear your feedback.

What do you need from the libraries? Let us know at the mailing list or please file an issue. Thanks!

Read more about the Map interface at the docs, and check out all the libraries at api.dartlang.org.

Popular posts from this blog

Lists and arrays in Dart

Converting Array to List in Scala

Null-aware operators in Dart