Part 18

Data Maps

Now that we are familiar with the standard type classes, we can look at one of their applications: First, the Map datastructure

Data.Map

The Data.Map module defines the Map type. Maps are search trees for key-value pairs. It is similar to the dictionaries you have seen in Python. One way to look at this is that a value of type Map k v is roughly the same as a value of type [(k,v)], a list of pairs. However, the operations on a map are more efficient than operations on a list.

Since Data.Map contains some function with the same names as Prelude functions, the namespace needs to be imported qualified:

import qualified Data.Map as Map

Now we can refer to the map type as Map.Map, and to various map functions like Map.insert. Here are the most important functions for maps:

-- Create a Map from a list of key-value pairs
Map.fromList :: Ord k => [(k, a)] -> Map.Map k a

-- Insert a value into a map. Overrides any previous value with the same key.
-- Returns a new map. Does not mutate the given map.
Map.insert :: Ord k => k -> a -> Map.Map k a -> Map.Map k a

-- Get a value from a map using a key. Returns Nothing if the key was not present in the map.
Map.lookup :: Ord k => k -> Map.Map k a -> Maybe a

-- An empty map
Map.empty :: Map.Map k a

The Ord constraint for the key type of the map is needed because maps are implemented as ordered binary search trees.

Note that like all Haskell values, maps are immutable meaning you can’t change a map once you define it. However, map operations like insert produce a new map. To perform multiple map operations you need to reuse the return value. Here’s a GHCi session operating on a map.

Prelude> import qualified Data.Map as Map
Prelude Map> values = Map.fromList [("z",3),("w",4)]
Prelude Map> Map.lookup "z" values
Just 3
Prelude Map> Map.lookup "banana" values
Nothing
Prelude Map> Map.insert "x" 7 values
fromList [("w",4),("x",7),("z",3)]
Prelude Map> values                                       -- note immutability!
fromList [("w",4),("z",3)]
Prelude Map> Map.insert "x" 1 (Map.insert "y" 2 values)   -- two insertions
fromList [("w",4),("x",1),("y",2),("z",3)]
Prelude Map>

Here’s an example of representing a bank as a Map String Int (map from account name to account balance), and withdrawing some money from an account:

withdraw :: String -> Int -> Map.Map String Int -> Map.Map String Int
withdraw account amount bank =
    case Map.lookup account bank of
    Nothing  -> bank                                   -- account not found, no change
    Just sum -> Map.insert account (sum-amount) bank   -- set new balance

Here’s how you might use the withdraw function in GHCi. Note how the maps get printed as fromList invocations. Also note how calling withdraw ... bank returns a new bank and doesn’t change the existing bank.

GHCi> bank = Map.fromList [("Bob",100),("Mike",50)]
GHCi> withdraw "Bob" 80 bank
fromList [("Bob",20),("Mike",50)]
GHCi> bank                         -- note immutability
fromList [("Bob",100),("Mike",50)]
GHCi> withdraw "Bozo" 1000 bank
fromList [("Bob",100),("Mike",50)]

Data.Map defines all sorts of useful higher-order functions for updating maps. We can rewrite the withdraw function using Data.Map.adjust:

withdraw :: String -> Int -> Map.Map String Int -> Map.Map String Int
withdraw account amount bank = Map.adjust (\x -> x-amount) account bank

Note! There are separate Data.Map.Strict and Data.Map.Lazy implementations. When you import Data.Map you get Data.Map.Lazy. You can find the documentation for all the Data.Map functions in the docs for Data.Map.Lazy. We won’t go into their differences here, but mostly you should use Data.Map.Strict in real code.

Exercises

All exercises can be found in Set4a and Set4b. Please pay attention in the title of the exercise in which file the exercises of this section can be found.

Exercises from 4a:

You have reached the end of this section! Continue to the next section:

You can check your current points from the blue blob in the bottom-right corner of the page.