Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dictionaries

A dictionary in Rex is an immutable mapping from String keys to values. Its type is Dict a, where a is the type of every value in the dictionary:

  • Keys are always String.
  • All values in one dictionary have the same type.
  • Operations return new dictionaries; they do not change their inputs.

For example, a Dict i32 can contain any number of integer values, but it cannot mix integers, strings, or other value types:

let scores = ({ alice = 10, bob = 12 }) is Dict i32 in scores

Dictionary literals use identifier-shaped keys. Functions such as dict_singleton, dict_insert, and dict_from_entries accept arbitrary String values as keys, including strings containing spaces.

Dictionary order

Rex stores dictionary entries in ascending lexicographic key order. This determines the order returned by dict_keys, dict_values, and dict_entries. It also makes collision handling in dict_map deterministic.

Function argument order

Rex functions are curried. Dictionary operations conventionally take the dictionary as their last argument, which makes partial application straightforward. For example:

let get_alice = dict_get "alice" in
get_alice (({ alice = 10, bob = 12 }) is Dict i32)

In the signatures below, a and b are type variables. Every occurrence of the same variable must be the same type.

Every example below is a complete executable Rex expression. You can edit it in place and run it to see the resulting value.

Quick reference

FunctionType
dict_emptyDict a
dict_singletonString -> a -> Dict a
dict_getString -> Dict a -> Option a
dict_hasString -> Dict a -> Bool
dict_insertString -> a -> Dict a -> Dict a
dict_removeString -> Dict a -> Dict a
dict_updateString -> (Option a -> Option a) -> Dict a -> Dict a
dict_is_emptyDict a -> Bool
dict_keysDict a -> List String
dict_valuesDict a -> List a
dict_entriesDict a -> List (String, a)
dict_from_entriesList (String, a) -> Dict a
dict_map((String, a) -> (String, b)) -> Dict a -> Dict b
dict_filter((String, a) -> Bool) -> Dict a -> Dict a
map on Dict(a -> b) -> Dict a -> Dict b
filter on Dict(a -> Bool) -> Dict a -> Dict a

Construction

dict_empty

Type: dict_empty : Dict a

dict_empty is an empty dictionary. Because it contains no values, its value type normally comes from the surrounding context or an explicit annotation.

let numbers: Dict i32 = dict_empty in numbers

dict_singleton

Type: dict_singleton : String -> a -> Dict a

dict_singleton key value constructs a dictionary containing exactly one entry.

dict_singleton "request id" "req-123"

dict_from_entries

Type: dict_from_entries : List (String, a) -> Dict a

dict_from_entries entries constructs a dictionary from a list of key/value tuples. It processes the list from first to last. If a key appears more than once, its last value wins. The resulting dictionary is stored in lexicographic key order, not list order.

dict_from_entries
  [("beta", 2), ("alpha", 1), ("beta", 20)]

Lookup and inspection

dict_get

Type: dict_get : String -> Dict a -> Option a

dict_get key dictionary returns Some value when the key exists and None when it does not. Lookup does not fail merely because a key is absent.

let scores = dict_from_entries [("alice", 10), ("bob", 12)] in
(dict_get "alice" scores, dict_get "carol" scores)

dict_has

Type: dict_has : String -> Dict a -> Bool

dict_has key dictionary reports whether the key exists, without retrieving its value.

let scores = dict_from_entries [("alice", 10), ("bob", 12)] in
(dict_has "bob" scores, dict_has "carol" scores)

dict_is_empty

Type: dict_is_empty : Dict a -> Bool

dict_is_empty dictionary is true only when the dictionary has no entries.

let empty: Dict String = dict_empty in
(dict_is_empty empty, dict_is_empty (dict_singleton "name" "Rex"))

dict_keys

Type: dict_keys : Dict a -> List String

dict_keys dictionary returns all keys in ascending lexicographic order.

dict_keys (dict_from_entries [("z", 1), ("alpha", 2), ("middle", 3)])

dict_values

Type: dict_values : Dict a -> List a

dict_values dictionary returns the values ordered by their corresponding keys. It does not sort the values themselves.

dict_values (dict_from_entries [("z", 1), ("alpha", 2), ("middle", 3)])

The result is [2, 3, 1], corresponding to keys alpha, middle, and z.

dict_entries

Type: dict_entries : Dict a -> List (String, a)

dict_entries dictionary returns key/value tuples in ascending lexicographic key order.

dict_entries (dict_from_entries [("z", 1), ("alpha", 2), ("middle", 3)])

Immutable updates

dict_insert

Type: dict_insert : String -> a -> Dict a -> Dict a

dict_insert key value dictionary returns a new dictionary. It adds an absent key or replaces the value of a present key. The input dictionary remains unchanged.

let
  original = dict_singleton "a" 1,
  added = dict_insert "b" 2 original,
  replaced = dict_insert "a" 99 original
in
  (original, added, replaced)

dict_remove

Type: dict_remove : String -> Dict a -> Dict a

dict_remove key dictionary returns a new dictionary without that key. Removing an absent key returns an equivalent dictionary and is not an error.

let original = dict_from_entries [("a", 1), ("b", 2)] in
(original, dict_remove "b" original, dict_remove "missing" original)

dict_update

Type: dict_update : String -> (Option a -> Option a) -> Dict a -> Dict a

dict_update key update dictionary calls update with Some current_value when the key is present or None when it is absent. The callback’s result controls the new dictionary:

  • Some new_value inserts or replaces the key.
  • None removes the key.
let
  original = dict_singleton "visits" 2,
  incremented = dict_update
    "visits"
    (\current -> match current with {
      case Some count -> Some (count + 1);
      case None -> Some 1;
    })
    original,
  removed = dict_update "visits" (\_ -> None) incremented
in
  (original, incremented, removed)

Transforming entries

The dict_map and dict_filter callbacks receive a two-element (String, a) tuple, so they can inspect both the key and value. Use a tuple pattern in a match expression to name both parts.

Callback applications for different entries may run in parallel. Rex functions are pure, so this does not change the result.

dict_map

Type: dict_map : ((String, a) -> (String, b)) -> Dict a -> Dict b

dict_map transform dictionary transforms both keys and values. The callback must return a (String, b) tuple, which becomes an entry in the result.

let
  input = (({ c = 3, a = 1, b = 2 }) is Dict i32),
  renamed = dict_map
    (\entry -> match entry with {
      case (key, value) -> ("item_" + key, value * 10);
    })
    input
in
  renamed

Multiple callbacks may return the same output key. Rex applies completed results in the input dictionary’s original lexicographic key order, regardless of callback completion order. Therefore, the result from the latest input key wins:

dict_map
  (\entry -> match entry with {
    case (key, value) -> ("same", key + ":" + show value);
  })
  (({ c = 3, a = 1, b = 2 }) is Dict i32)

The input order is a, b, c, so the final dictionary contains "c:3" at key same.

dict_filter

Type: dict_filter : ((String, a) -> Bool) -> Dict a -> Dict a

dict_filter predicate dictionary keeps each original entry for which the predicate returns true. The predicate can inspect both key and value. Accepted entries keep their original keys and values.

dict_filter
  (\entry -> match entry with {
    case (key, value) -> key != "draft" && value >= 2;
  })
  (({ draft = 10, first = 1, second = 2, third = 3 }) is Dict i32)

Transforming values with typeclass functions

Dict implements Functor and Filterable. Their generic functions operate on values only; the callback does not receive a key. These functions preserve every retained entry’s original key.

Like the entry-aware functions, callback applications for different values may run in parallel.

map on dictionaries

Type: map : (a -> b) -> Dict a -> Dict b

map transform dictionary transforms every value and preserves every key. Use dict_map instead when the callback needs the key or must produce new keys.

map
  (\score -> "score=" + show score)
  (({ alice = 10, bob = 12 }) is Dict i32)

filter on dictionaries

Type: filter : (a -> Bool) -> Dict a -> Dict a

filter predicate dictionary tests values only and preserves the keys of accepted values. Use dict_filter when filtering depends on a key.

filter
  (\score -> score >= 10)
  (({ alice = 10, bob = 7, carol = 12 }) is Dict i32)

Choosing the right transformation

NeedFunction
Transform values and preserve all keysmap
Keep entries based only on valuesfilter
Transform keys and/or inspect keys while mappingdict_map
Keep entries based on keys and/or valuesdict_filter

For dictionary literal syntax and dictionary pattern matching, see Collections.