turboMaCk / any-dict / Dict.Any

A dictionary mapping unique keys to values. Similar and based on Dict but without restriction on comparable keys.

Insert, remove, and query operations all take O(log n) time.

Converting Types to Comparable

When writing a function for conversion from the type you want to use for keys to comparable it's very important to make sure every distinct member of type k produces different value in set o of comparables.

Take for instance those two examples:

We can use Bool as a key for our Dict (No matter how unpractical it might seem)

boolToInt : Bool -> Int
boolToInt bool =
    case bool of
        False -> 0
        True -> 1

empty boolToInt
|> insert True "foo"
|> get True
--> Just "foo"

or Maybe String.

comparableKey : Maybe String -> (Int, String)
comparableKey maybe =
    case maybe of
        Nothing -> (0, "")
        Just str -> (1, str)

empty comparableKey
    |> insert (Just "foo") 42
    |> get (Just "foo")
--> Just 42

Note that we give Int code to either constructor and in Case of Nothing we default to "" (empty string). There is still a difference between Nothing and Just "" (Int value in the pair is different). In fact, you can "hardcode" any value as the second member of the pair in case of nothing but empty string seems like a reasonable option for this case. Generally, this is how I would implement toComparable function for most of your custom data types. Have a look at the longest constructor, Define tuple where the first key is int (number of the constructor) and other are types within the constructor and you're good to go.

Dictionaries


type AnyDict comparable k v

Be aware that AnyDict stores a function internally.

Use equal function to check equality of two AnyDicts. Using (==) would result in runtime exception because AnyDict type contains a function.

equal : AnyDict comparable k v -> AnyDict comparable k v -> Basics.Bool

Check equality of two AnyDicts

* returns `True` if AnyDicts are equal
* returns `False` if AnyDicts are not equal

Build

empty : (k -> comparable) -> AnyDict comparable k v

Create an empty dictionary by suppling function used for comparing keys.

Note that it's important to make sure every key is turned to different comparable. Otherwise keys would conflict and overwrite each other.

singleton : k -> v -> (k -> comparable) -> AnyDict comparable k v

Create a dictionary with one key-value pair.

Note that it's important to make sure every key is turned to different comparable. Otherwise keys would conflict and overwrite each other.

insert : k -> v -> AnyDict comparable k v -> AnyDict comparable k v

Insert a key-value pair into a dictionary. Replaces value when there is a collision.

update : k -> (Maybe v -> Maybe v) -> AnyDict comparable k v -> AnyDict comparable k v

Update the value of a dictionary for a specific key with a given function.

remove : k -> AnyDict comparable k v -> AnyDict comparable k v

Remove a key-value pair from a dictionary. If the key is not found, no changes are made.

removeAll : AnyDict comparable k v -> AnyDict comparable k x

Remove all entries from AnyDict.

Useful when you need to create new empty AnyDict using same comparable function for key type.

Query

isEmpty : AnyDict comparable k v -> Basics.Bool

Determine if a dictionary is empty.

isEmpty (empty identity)
--> True

singleton 1 "foo" identity
    |> isEmpty
--> False

member : k -> AnyDict comparable k v -> Basics.Bool

Determine if a key is in a dictionary.

get : k -> AnyDict comparable k v -> Maybe v

Get the value associated with a key. If the key is not found, return Nothing. This is useful when you are not sure if a key will be in the dictionary.

type Animal = Cat | Mouse | Dog

animalToInt : Animal -> Int
animalToInt animal =
    case animal of
        Cat -> 0
        Mouse -> 1
        Dog -> 2

animals : AnyDict Int Animal String
animals =
    [ (Cat, "Tom"), (Mouse, "Jerry") ]
        |> fromList animalToInt

get Cat animals
-> Just "Tom"

get Mouse animals
--> Just "Jerry"

get Dog animals
--> Nothing

getKey : k -> AnyDict comparable k v -> Maybe k

Get a key associated with key.

This is useful in case of AnyDict because some parts of a key might not be used for generating comparable. This function allows quering AnyDict with old key to obtain updated one in such cases.

size : AnyDict comparable k v -> Basics.Int

Determine the number of key-value pairs in the dictionary.

any : (k -> v -> Basics.Bool) -> AnyDict comparable k v -> Basics.Bool

Find out if there is any instance of something in a Dictionary.

type Animal = Cat | Mouse | Dog

animalToInt : Animal -> Int
animalToInt animal =
    case animal of
        Cat -> 0
        Mouse -> 1
        Dog -> 2

animals : AnyDict Int Animal String
animals =
    [ (Cat, "Tom"), (Mouse, "Jerry") ]
        |> fromList animalToInt

isACat : Animal -> String -> Bool
isACat animal _ =
    case animal of
        Cat -> True
        _ -> False

any isACat animals
--> True

all : (k -> v -> Basics.Bool) -> AnyDict comparable k v -> Basics.Bool

Find out if all instances of a Dictionary match a predicate.

type Animal = Cat | Mouse | Dog

animalToInt : Animal -> Int
animalToInt animal =
    case animal of
        Cat -> 0
        Mouse -> 1
        Dog -> 2

animals : AnyDict Int Animal String
animals =
    [ (Cat, "Tom"), (Mouse, "Jerry") ]
        |> fromList animalToInt

aristocats : AnyDict Int Animal String
aristocats =
    [ (Cat, "Marie"), (Cat, "Duchess"), (Cat, "Toulouse"), (Cat, "Berlioz") ]
        |> fromList animalToInt

isACat : Animal -> String -> Bool
isACat animal _ =
    case animal of
        Cat -> True
        _ -> False

all isACat animals
--> False

all isACat aristocats
--> True

Lists

keys : AnyDict comparable k v -> List k

Get all of the keys in a dictionary, sorted from lowest to highest.

values : AnyDict comparable k v -> List v

Get all of the values in a dictionary, in the order of their keys.

toList : AnyDict comparable k v -> List ( k, v )

Convert a dictionary into an association list of key-value pairs, sorted by keys.

fromList : (k -> comparable) -> List ( k, v ) -> AnyDict comparable k v

Convert an association list into a dictionary.

Note that it's important to make sure every key is turned to different comparable. Otherwise keys would conflict and overwrite each other.

Transform

map : (a -> b -> c) -> AnyDict comparable a b -> AnyDict comparable a c

Apply a function to all values in a dictionary.

foldl : (k -> v -> b -> b) -> b -> AnyDict comparable k v -> b

Fold over the key-value pairs in a dictionary, in order from lowest key to highest key.

foldr : (k -> v -> b -> b) -> b -> AnyDict comparable k v -> b

Fold over the key-value pairs in a dictionary, in order from highest key to lowest key.

filter : (k -> v -> Basics.Bool) -> AnyDict comparable k v -> AnyDict comparable k v

Keep a key-value pair when it satisfies a predicate.

partition : (k -> v -> Basics.Bool) -> AnyDict comparable k v -> ( AnyDict comparable k v, AnyDict comparable k v )

Partition a dictionary according to a predicate. The first dictionary contains all key-value pairs which satisfy the predicate, and the second contains the rest.

filterMap : (k -> v1 -> Maybe v2) -> AnyDict comparable k v1 -> AnyDict comparable k v2

Apply a function that may or may not succeed to all entries in a dictionary, but only keep the successes.

type Animal = Cat | Mouse | Dog

animalToInt : Animal -> Int
animalToInt animal =
    case animal of
        Cat -> 0
        Mouse -> 1
        Dog -> 2

animals : AnyDict Int Animal String
animals =
    [ (Cat, "Tom"), (Mouse, "Jerry") ]
        |> fromList animalToInt

onlyTom : AnyDict Int Animal String
onlyTom =
    [ (Cat, "Tom") ]
        |> fromList animalToInt

getCatName : Animal -> String -> Maybe String
getCatName animal name =
    case animal of
        Cat -> Just name
        _ -> Nothing

filterMap getCatName animals == onlyTom
--> True

Combine

union : AnyDict comparable k v -> AnyDict comparable k v -> AnyDict comparable k v

Combine two dictionaries. If there is a collision, preference is given to the first dictionary.

intersect : AnyDict comparable k v -> AnyDict comparable k v -> AnyDict comparable k v

Keep a key-value pair when its key appears in the second dictionary. Preference is given to values in the first dictionary.

diff : AnyDict comparable k v -> AnyDict comparable k v -> AnyDict comparable k v

Keep a key-value pair when its key does not appear in the second dictionary.

merge : (k -> a -> result -> result) -> (k -> a -> b -> result -> result) -> (k -> b -> result -> result) -> AnyDict comparable k a -> AnyDict comparable k b -> result -> result

The most general way of combining two dictionaries. You provide three accumulators for when a given key appears:

  1. Only in the left dictionary.
  2. In both dictionaries.
  3. Only in the right dictionary.

Only in the left dictionary. In both dictionaries. Only in the right dictionary.

groupBy : (value -> key) -> (key -> comparable) -> List value -> AnyDict comparable key (List value)

Takes a key-fn and a list. Creates an AnyDict which maps the key to a list of matching elements.

Dict

toDict : AnyDict comparable k v -> Dict comparable v

Convert AnyDict to plain dictionary with comparable keys.

Json

decode : (String -> v -> k) -> (k -> comparable) -> Json.Decode.Decoder v -> Json.Decode.Decoder (AnyDict comparable k v)

Decode a JSON object into an AnyDict.

This encoder is limitted for cases where JSON representation for a given type is an JSON Object. In JSON, object keys must be of type String. If you need to decode different representation into AnyDict value, just use primitive Decoder types directly and map AnyDict constructors over these.

import Json.Decode

type Key = Foo | Bar

fromString : String -> Key
fromString str =
   case str of
      "foo" -> Foo
      _     -> Bar

toString : Key -> String
toString key =
  case key of
     Foo -> "foo"
     Bar -> "bar"

type alias Data = AnyDict String Key Int

dataDecoder : Json.Decode.Decoder Data
dataDecoder =
   decode (\str _ -> fromString str) toString Json.Decode.int


Json.Decode.decodeString dataDecoder "{\"foo\":1,\"bar\":2}"
|> Result.map toList
--> Ok [(Bar, 2), (Foo, 1)]

decode_ : (String -> v -> Result String k) -> (k -> comparable) -> Json.Decode.Decoder v -> Json.Decode.Decoder (AnyDict comparable k v)

Decode a JSON object into an AnyDict.

This variant of decode allows you to fail with error while parsing key from String. In such case whole Dict decoding will fail.

import Json.Decode

type Key = Foo | Bar

fromString : String -> Result String Key
fromString str =
   case str of
      "foo" -> Ok Foo
      "bar" -> Ok Bar
      _     -> Err <| "Unknown key " ++ str

toString : Key -> String
toString key =
  case key of
     Foo -> "foo"
     Bar -> "bar"

type alias Data = AnyDict String Key Int

dataDecoder : Json.Decode.Decoder Data
dataDecoder =
   decode_ (\str _ -> fromString str) toString Json.Decode.int


Json.Decode.decodeString dataDecoder "{\"foo\":1,\"bar\":2}"
|> Result.map toList
--> Ok [(Bar, 2), (Foo, 1)]

Json.Decode.decodeString dataDecoder "{\"foo\":1,\"baz\":2}"
|> Result.map toList
--> Json.Decode.decodeString (Json.Decode.fail "Unknown key baz") "{}"

decodeList : (k -> comparable) -> Json.Decode.Decoder ( k, v ) -> Json.Decode.Decoder (AnyDict comparable k v)

Decode an AnyDict from a JSON list of tuples.

import Json.Decode as Decode
import Json.Encode as Encode

type alias Person = {first : String, last : String}
type alias Age = Int

personToString : Person -> String
personToString {first, last} = first ++ last

personDecode : Decode.Decoder Person
personDecode =
    Decode.map2
        Person
            (Decode.field "first" Decode.string)
            (Decode.field "last" Decode.string)

"[[{\"first\":\"Jeve\",\"last\":\"Sobs\"},9001],[{\"first\":\"Tim\",\"last\":\"Berners-Lee\"},1234]]"
    |> Decode.decodeString (decodeList personToString (Decode.map2 Tuple.pair (Decode.index 0 personDecode) (Decode.index 1 Decode.int)))
    --> Ok (fromList personToString [(Person "Jeve" "Sobs", 9001), (Person "Tim" "Berners-Lee", 1234)])

encode : (k -> String) -> (v -> Json.Encode.Value) -> AnyDict comparable k v -> Json.Encode.Value

Turn an AnyDict into a JSON object.

import Json.Encode

type Key = Foo | Bar

toString : Key -> String
toString key =
  case key of
     Foo -> "foo"
     Bar -> "bar"

type alias Data = AnyDict String Key Int

encodeData : Data -> Json.Encode.Value
encodeData =
  encode toString Json.Encode.int

fromList toString [(Foo, 1), (Bar, 2)]
|> encodeData
|> Json.Encode.encode 0
--> "{\"bar\":2,\"foo\":1}"

encodeList : (k -> v -> Json.Encode.Value) -> AnyDict comparable k v -> Json.Encode.Value

Turn an AnyDict into a JSON list of tuples. This is useful when you have more complex types as keys

import Json.Decode as Decode
import Json.Encode as Encode

type alias Person = {first : String, last : String}
type alias Age = Int

personToString : Person -> String
personToString {first, last} = first ++ last

personEncode : Person -> Encode.Value
personEncode {first, last} =
    Encode.object [("first", (Encode.string first)), ("last", (Encode.string last))]

example : AnyDict String Person Age
example =
    fromList personToString [(Person "Jeve" "Sobs", 9001), (Person "Tim" "Berners-Lee", 1234)]

encodeList (\k v -> Encode.list identity [ personEncode k, Encode.int v ]) example
    |> Encode.encode 0
    --> "[[{\"first\":\"Jeve\",\"last\":\"Sobs\"},9001],[{\"first\":\"Tim\",\"last\":\"Berners-Lee\"},1234]]"