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_opt
returns the associated value, wrapped in an optionfind
returns the associated value
When the searched key is absent from the map:
find_opt
returnsNone
find
throws theNot_found
exceptions
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_fst
picks the result's value from the first mappick_snd
picks the result's value from the second mapdrop
drops 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
t
exposing the type of the map's key - A function
compare : t -> t -> int
function that comparest
values
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.