Dict

Dict k v

A dictionary that lets you associate keys with values.

Inserting

The most basic way to use a dictionary is to start with an empty one and then:

  1. Call Dict.insert passing a key and a value, to associate that key with that value in the dictionary.
  2. Later, call Dict.get passing the same key as before, and it will return the value you stored.

Here's an example of a dictionary which uses a city's name as the key, and its population as the associated value.

populationByCity =
    Dict.empty {}
    |> Dict.insert "London" 8_961_989
    |> Dict.insert "Philadelphia" 1_603_797
    |> Dict.insert "Shanghai" 24_870_895
    |> Dict.insert "Delhi" 16_787_941
    |> Dict.insert "Amsterdam" 872_680

Accessing keys or values

We can use Dict.keys and Dict.values functions to get only the keys or only the values.

You may notice that these lists have the same order as the original insertion order. This will be true if all you ever do is Dict.insert and Dict.get operations on the dictionary, but Dict.remove operations can change this order.

Removing

We can remove an element from the dictionary, like so:

populationByCity
    |> Dict.remove "Philadelphia"
    |> Dict.keys
    ==
    ["London", "Amsterdam", "Shanghai", "Delhi"]

Notice that the order has changed. Philadelphia was not only removed from the list, but Amsterdam - the last entry we inserted - has been moved into the spot where Philadelphia was previously. This is exactly what Dict.remove does. It removes an element and moves the most recent insertion into the vacated spot.

This move is done as a performance optimization, and it lets remove have constant time complexity.

Dict is inspired by IndexMap. The internal implementation of a dictionary is almost identical to ankerl::unordered_dense. It has a list of keys value pairs that is ordered based on insertion. It uses a list of indices into the data as the backing of a hash map.

empty : {} -> Dict * *

Return an empty dictionary.

emptyDict = Dict.empty {}

withCapacity : U64 -> Dict * *

Return a dictionary with space allocated for a number of entries. This may provide a performance optimization if you know how many entries will be inserted.

reserve : Dict k v, U64 -> Dict k v

Enlarge the dictionary for at least capacity additional elements

releaseExcessCapacity : Dict k v -> Dict k v

Shrink the memory footprint of a dictionary such that capacity is as small as possible. This function will require regenerating the metadata if the size changes. There will still be some overhead due to dictionary metadata always being a power of 2.

capacity : Dict * * -> U64

Returns the max number of elements the dictionary can hold before requiring a rehash.

foodDict =
    Dict.empty {}
    |> Dict.insert "apple" "fruit"

capacityOfDict = Dict.capacity foodDict

single : k, v -> Dict k v

Returns a dictionary containing the key and value provided as input.

expect
    Dict.single "A" "B"
    |> Bool.isEq (Dict.insert (Dict.empty {}) "A" "B")

fromList : List ( k, v ) -> Dict k v

Returns dictionary with the keys and values specified by the input List.

expect
    Dict.single 1 "One"
    |> Dict.insert 2 "Two"
    |> Dict.insert 3 "Three"
    |> Dict.insert 4 "Four"
    |> Bool.isEq (Dict.fromList [(1, "One"), (2, "Two"), (3, "Three"), (4, "Four")])

Performance Details

This will build up from an empty dictionary to minimize totally memory use. If the list has few duplicate keys, it would be faster to allocate a dictionary with the same capacity of the list and walk it calling Dict.insert

len : Dict * * -> U64

Returns the number of values in the dictionary.

expect
    Dict.empty {}
    |> Dict.insert "One" "A Song"
    |> Dict.insert "Two" "Candy Canes"
    |> Dict.insert "Three" "Boughs of Holly"
    |> Dict.len
    |> Bool.isEq 3

isEmpty : Dict * * -> Bool

Check if the dictionary is empty.

Dict.isEmpty (Dict.empty {} |> Dict.insert "key" 42)

Dict.isEmpty (Dict.empty {})

clear : Dict k v -> Dict k v

Clears all elements from a dictionary keeping around the allocation if it isn't huge.

songs =
       Dict.empty {}
       |> Dict.insert "One" "A Song"
       |> Dict.insert "Two" "Candy Canes"
       |> Dict.insert "Three" "Boughs of Holly"

clearSongs = Dict.clear songs

expect Dict.len clearSongs == 0

map : Dict k a, (k, a -> b) -> Dict k b

Convert each value in the dictionary to something new, by calling a conversion function on each of them which receives both the key and the old value. Then return a new dictionary containing the same keys and the converted values.

joinMap : Dict a b, (a, b -> Dict x y) -> Dict x y

Like Dict.map, except the transformation function wraps the return value in a dictionary. At the end, all the dictionaries get joined together (using Dict.insertAll) into one dictionary.

You may know a similar function named concatMap in other languages.

walk : Dict k v, state, (state, k, v -> state) -> state

Iterate through the keys and values in the dictionary and call the provided function with signature state, k, v -> state for each value, with an initial state value provided for the first call.

expect
    Dict.empty {}
    |> Dict.insert "Apples" 12
    |> Dict.insert "Orange" 24
    |> Dict.walk 0 (\count, _, qty -> count + qty)
    |> Bool.isEq 36

walkUntil : Dict k v, state, (state, k, v -> [ Continue state, Break state ]) -> state

Same as Dict.walk, except you can stop walking early.

Performance Details

Compared to Dict.walk, this can potentially visit fewer elements (which can improve performance) at the cost of making each step take longer. However, the added cost to each step is extremely small, and can easily be outweighed if it results in skipping even a small number of elements.

As such, it is typically better for performance to use this over Dict.walk if returning Break earlier than the last element is expected to be common.

people =
    Dict.empty {}
    |> Dict.insert "Alice" 17
    |> Dict.insert "Bob" 18
    |> Dict.insert "Charlie" 19

isAdult = \_, _, age ->
        if age >= 18 then
            Break Bool.true
        else
            Continue Bool.false

someoneIsAnAdult = Dict.walkUntil people Bool.false isAdult

expect someoneIsAnAdult == Bool.true

keepIf : Dict k v, ( ( k, v ) -> Bool) -> Dict k v

Run the given function on each key-value pair of a dictionary, and return a dictionary with just the pairs for which the function returned Bool.true.

expect Dict.empty {}
    |> Dict.insert "Alice" 17
    |> Dict.insert "Bob" 18
    |> Dict.insert "Charlie" 19
    |> Dict.keepIf \(_k, v) -> v >= 18
    |> Dict.len
    |> Bool.isEq 2

dropIf : Dict k v, ( ( k, v ) -> Bool) -> Dict k v

Run the given function on each key-value pair of a dictionary, and return a dictionary with just the pairs for which the function returned Bool.false.

expect Dict.empty {}
    |> Dict.insert "Alice" 17
    |> Dict.insert "Bob" 18
    |> Dict.insert "Charlie" 19
    |> Dict.dropIf \(_k, v) -> v >= 18
    |> Dict.len
    |> Bool.isEq 1

get : Dict k v, k -> Result v [KeyNotFound]

Get the value for a given key. If there is a value for the specified key it will return [Ok value], otherwise return [Err KeyNotFound].

dictionary =
    Dict.empty {}
    |> Dict.insert 1 "Apple"
    |> Dict.insert 2 "Orange"

expect Dict.get dictionary 1 == Ok "Apple"
expect Dict.get dictionary 2000 == Err KeyNotFound

contains : Dict k v, k -> Bool

Check if the dictionary has a value for a specified key.

expect
    Dict.empty {}
    |> Dict.insert 1234 "5678"
    |> Dict.contains 1234
    |> Bool.isEq Bool.true

insert : Dict k v, k, v -> Dict k v

Insert a value into the dictionary at a specified key.

expect
    Dict.empty {}
    |> Dict.insert "Apples" 12
    |> Dict.get "Apples"
    |> Bool.isEq (Ok 12)

remove : Dict k v, k -> Dict k v

Remove a value from the dictionary for a specified key.

expect
    Dict.empty {}
    |> Dict.insert "Some" "Value"
    |> Dict.remove "Some"
    |> Dict.len
    |> Bool.isEq 0

update : Dict k v, k, (Result v [Missing] -> Result v [Missing]) -> Dict k v

Insert or remove a value for a specified key. This function enables a performance optimization for the use case of providing a default when a value is missing. This is more efficient than doing both a Dict.get and then a Dict.insert call, and supports being piped.

alterValue : Result Bool [Missing] -> Result Bool [Missing]
alterValue = \possibleValue ->
    when possibleValue is
        Err Missing -> Ok Bool.false
        Ok value -> if value then Err Missing else Ok Bool.true

expect Dict.update (Dict.empty {}) "a" alterValue == Dict.single "a" Bool.false
expect Dict.update (Dict.single "a" Bool.false) "a" alterValue == Dict.single "a" Bool.true
expect Dict.update (Dict.single "a" Bool.true) "a" alterValue == Dict.empty {}

toList : Dict k v -> List ( k, v )

Returns the keys and values of a dictionary as a List. This requires allocating a temporary list, prefer using Dict.toList or Dict.walk instead.

expect
    Dict.single 1 "One"
    |> Dict.insert 2 "Two"
    |> Dict.insert 3 "Three"
    |> Dict.insert 4 "Four"
    |> Dict.toList
    |> Bool.isEq [(1, "One"), (2, "Two"), (3, "Three"), (4, "Four")]

keys : Dict k v -> List k

Returns the keys of a dictionary as a List. This requires allocating a temporary List, prefer using Dict.toList or Dict.walk instead.

expect
    Dict.single 1 "One"
    |> Dict.insert 2 "Two"
    |> Dict.insert 3 "Three"
    |> Dict.insert 4 "Four"
    |> Dict.keys
    |> Bool.isEq [1,2,3,4]

values : Dict k v -> List v

Returns the values of a dictionary as a List. This requires allocating a temporary List, prefer using Dict.toList or Dict.walk instead.

expect
    Dict.single 1 "One"
    |> Dict.insert 2 "Two"
    |> Dict.insert 3 "Three"
    |> Dict.insert 4 "Four"
    |> Dict.values
    |> Bool.isEq ["One","Two","Three","Four"]

insertAll : Dict k v, Dict k v -> Dict k v

Combine two dictionaries by keeping the union of all the key-value pairs. This means that all the key-value pairs in both dictionaries will be combined. Note that where there are pairs with the same key, the value contained in the second input will be retained, and the value in the first input will be removed.

first =
    Dict.single 1 "Not Me"
    |> Dict.insert 2 "And Me"

second =
    Dict.single 1 "Keep Me"
    |> Dict.insert 3 "Me Too"
    |> Dict.insert 4 "And Also Me"

expected =
    Dict.single 1 "Keep Me"
    |> Dict.insert 2 "And Me"
    |> Dict.insert 3 "Me Too"
    |> Dict.insert 4 "And Also Me"

expect
    Dict.insertAll first second == expected

keepShared : Dict k v, Dict k v -> Dict k v where v implements Eq

Combine two dictionaries by keeping the intersection of all the key-value pairs. This means that we keep only those pairs that are in both dictionaries. Both the key and value must match to be kept.

first =
    Dict.single 1 "Keep Me"
    |> Dict.insert 2 "And Me"
    |> Dict.insert 3 "Not this one"

second =
    Dict.single 1 "Keep Me"
    |> Dict.insert 2 "And Me"
    |> Dict.insert 3 "This has a different value"
    |> Dict.insert 4 "Or Me"

expected =
    Dict.single 1 "Keep Me"
    |> Dict.insert 2 "And Me"

expect Dict.keepShared first second == expected

removeAll : Dict k v, Dict k v -> Dict k v

Remove the key-value pairs in the first input that are also in the second using the set difference of the values. This means that we will be left with only those pairs that are in the first dictionary and whose keys are not in the second.

first =
    Dict.single 1 "Keep Me"
    |> Dict.insert 2 "And Me"
    |> Dict.insert 3 "Remove Me"

second =
    Dict.single 3 "Remove Me"
    |> Dict.insert 4 "I do nothing..."

expected =
    Dict.single 1 "Keep Me"
    |> Dict.insert 2 "And Me"

expect Dict.removeAll first second == expected