Maps
Introduction
In the most general sense, the Map module lets you create immutable key-value
associative array for your types. More concretely,
OCaml's Map module is implemented using a binary search tree alogorithm to support fast lookups (of O(Log n)).
Note: The concept of a Map in this tutorial refers to a data structure that stores a
set of key-value pairs. It is sometimes called a dictionary or an association table. This
is a disinct concept from the maps we've explored in previous lessons, such as List.map,
Array.map, and Option.map, which are functions that operate over data rather than
being data themselves.
To use Map, we first have to use the Map.Make functor to
create our custom map module. Refer to the Functors for more information
on functors. This functor has a module parameter that defines the keys' type to be used in
the maps, and a function for comparing them.
For an different implementation of an association table in OCaml's Standard Library, see the tutorial on Hash Tables.
# module StringMap = Map.Make(String);;
module StringMap :
  sig
    type key = string
    type 'a t = 'a Map.Make(String).t
    val empty : 'a t
    val add : key -> 'a -> 'a t -> 'a t
    val add_to_list : key -> 'a -> 'a list t -> 'a list t
    val update : key -> ('a option -> 'a option) -> 'a t -> 'a t
    val singleton : key -> 'a -> 'a t
    val remove : key -> 'a t -> 'a t
    (* ... *)
  end
After naming the newly-created module StringMap, OCaml's toplevel displays the
module's signature. Since it contains a large number of functions, the output
copied here is shortened for brevity (...).
When we created the StringMap module, we fed the Map.Make functor the String module to
define the type of the map's keys, which we can observe in the StringMap's signature
(type key string). However, we did not yet define the value's type. The value's type will
be defined when we create our first map.
Creating a Map
The StringMap module has an empty value that has a type parameter 'a in
its type: empty : 'a t.
This means that we can use empty to create new empty maps where the value is of any type.
The type of the values can be specified in two ways:
- When you create your new map with an annotation
- By adding an element to the map:
Example 1: By Annotation
# let int_map : int StringMap.t = StringMap.empty;;
val int_map : int StringMap.t = <abstr>
Example 2: By Adding an Element
# let int_map = StringMap.(empty |> add "one" 1);;
val int_map : int StringMap.t = <abstr>
Working With Maps
Throughout the rest of this tutorial, we will use the following map:
# let lucky_numbers = StringMap.of_seq @@ List.to_seq [
    ("leostera", 2112);
    ("charstring88", 88);
    ("divagnz", 13);
  ];;
val lucky_numbers : int StringMap.t = <abstr>
Finding Entries in a Map
To find entries in a map, use the find_opt or find functions:
# StringMap.find_opt "leostera" lucky_numbers;;
- : int option = Some 2112
# StringMap.find "leostera" lucky_numbers;;
- : int = 2112
When the searched key is present from the map:
- find_optreturns the associated value, wrapped in an option
- findreturns the associated value
When the searched key is absent from the map:
- find_optreturns- None
- findthrows the- Not_foundexceptions
We can also use find_first_opt and find_last_opt if we want to use a
predicate function:
# let first_under_10_chars : (string * int) option =
  StringMap.find_first_opt
    (fun key -> String.length key < 10)
    lucky_numbers;;
val first_under_10_chars : (string * int) option = Some ("divagnz", 13)
The functions find_first and find_last behave similarly, except they
throw exceptions instead of returning options.
Note that find_first_opt and find_last_opt return the key-value pair,
not just the value.
Adding Entries to a Map
To add an entry to a map, use the add function that takes a key, a value, and
the map to which it will be added. It returns a new map with that key-value pair added:
# let more_lucky_numbers = lucky_numbers |> StringMap.add "paguzar" 108;;
val more_lucky_numbers : int StringMap.t = <abstr>
# StringMap.find_opt "paguzar" lucky_numbers;;
- : int option = None
# StringMap.find_opt "paguzar" more_lucky_numbers;;
- : int option = Some 108
If the passed key is already associated with a value, the passed value replaces it.
Note that the initial map lucky_numbers remains unchanged.
Removing Entries From a Map
To remove an entry from a map, use the remove function, which takes a key and
a map. It returns a new map with that key's entry removed.
# let fewer_lucky_numbers = lucky_numbers |> StringMap.remove "divagnz";;
val fewer_lucky_numbers : int StringMap.t = <abstr>
# StringMap.find_opt "divagnz" lucky_numbers;;
- : int option = Some 13
# StringMap.find_opt "divagnz" fewer_lucky_numbers;;
- : int option = None
Removing a key that isn't present in the map has no effect.
Note that the initial map lucky_numbers remains unchanged.
Changing the Value Associated With a Key
To change a key's associated value, use the update function. It takes a
key, a map, and an update function. It returns a new map with the key's
associated value replaced by the new one.
# let updated_lucky_numbers =
    lucky_numbers
    |> StringMap.update "charstring88" (Option.map (fun _ -> 99));;
val updated_lucky_numbers : int StringMap.t = <abstr>
# StringMap.find_opt "charstring88" lucky_numbers;;
- : int option = Some 88
# StringMap.find_opt "charstring88" updated_lucky_numbers;;
- : int option = Some 99
You should experiment with different update functions; several behaviors are possible.
Checking if a Key is Contained in a Map
To check if a key is a member of a map, use the mem function:
# StringMap.mem "paguzar" less_lucky_numbers;;
- : bool = false
Merging Maps
To merge two maps, use the union function. This function takes two maps, a
function deciding how to handle entries with identical keys, and it returns a new map.
Note: As with all the other functions of Map, the input maps are not modified.
# StringMap.union;;
- : (string -> 'a -> 'a -> 'a option) ->
    'a StringMap.t -> 'a StringMap.t -> 'a StringMap.t
= <fun>
Here are examples of duplicate key resolution functions:
# let pick_fst key v1 _ = Some v1;;
val pick_fst : 'a -> 'b -> 'c -> 'b option = <fun>
# let pick_snd key _ v2 = Some v2;;
val pick_snd : 'a -> 'b -> 'c -> 'c option = <fun>
# let drop _ _ _ = None;;
val drop : 'a -> 'b -> 'c -> 'd option = <fun>
- pick_fstpicks the result's value from the first map
- pick_sndpicks the result's value from the second map
- dropdrops both entries in the result map
# StringMap.(
    union pick_fst lucky_numbers updated_lucky_numbers
    |> find_opt "charstring88"
  );;
- : int option = Some 88
# StringMap.(
    union pick_snd lucky_numbers updated_lucky_numbers
    |> find_opt "charstring88"
  );;
- : int option = Some 99
# StringMap.(
    union drop lucky_numbers updated_lucky_numbers
    |> find_opt "charstring88"
  );;
- : int option = None
Filtering a Map
To filter a map, use the filter function. It takes a predicate to filter
entries and a map. It returns a new map containing the entries satisfying the
predicate.
# let even_numbers =
  StringMap.filter
    (fun _ number -> number mod 2 = 0)
    lucky_numbers;;
val even_numbers : int StringMap.t = <abstr>
Map a Map
Map modules have a map function:
StringMap.map;;
- : ('a -> 'b) -> 'a StringMap.t -> 'b StringMap.t = <fun>
The lucky_numbers map associates string keys with integer values:
# lucky_numbers;;
- : int StringMap.t = <abstr>
Using StringMap.map, we create a map associating keys with string values:
# let lucky_strings = StringMap.map string_of_int lucky_numbers;;
val lucky_strings : string StringMap.t = <abstr>
The keys are the same in both maps. For each key, a value in lucky_numbers is converted into a value in lucky_strings using string_of_int.
# lucky_numbers |> StringMap.find "leostera" |> string_of_int;;
- : string = "2112"
# lucky_strings |> StringMap.find "leostera";;
- : string = "2112"
Maps With Custom Key Types
If you need to create a map with a custom key type, you can call the Map.Make
functor with a module of your own, provided that it implements two things:
- A type texposing the type of the map's key
- A function compare : t -> t -> intfunction that comparestvalues
Let's define our custom map below for non-negative numbers.
We'll start by defining a module for strings that compares them in case-insensitive way.
# module Istring = struct
    type t = string
    let compare a b = String.(compare (lowercase_ascii a) (lowercase_ascii b))
  end;;
module Istring : sig type t val compare : t -> t -> int end
Note that our module has a type t and also a compare function. Now we can
call the Map.Make functor to get a map for non-negative numbers:
# module IstringMap = Map.Make(Istring);;
module IstringMap :
  sig
    type key = Istring.t
    type 'a t = 'a Map.Make(Istring).t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val mem : key -> 'a t -> bool
    val add : key -> 'a -> 'a t -> 'a t
    val update : key -> ('a option -> 'a option) -> 'a t -> 'a t
    val singleton : key -> 'a -> 'a t
    val remove : key -> 'a t -> 'a t
    (* ... *)
end
# let lucky_int_numbers = IstringMap.of_seq @@ List.to_seq [
    ("leostera", 2112);
    ("charstring88", 88);
    ("divagnz", 13);
  ];;
val lucky_int_numbers : int IstringMap.t = <abstr>
Conclusion
This was an overview of OCaml's Map module. Maps are reasonably efficient and
can be an alternative to the imperative Hashtbl module.
For more information, refer to Map in the Standard Library documentation.
Help Improve Our Documentation
All OCaml docs are open source. See something that's wrong or unclear? Submit a pull request.