From 41ede5bbff352c94547d2999c584a9c36362b7de Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Thu, 14 Dec 2023 18:35:27 +0100 Subject: [PATCH] Add Hashdict --- elm.json | 1 + src/Internal/Tools/Hashdict.elm | 228 ++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 src/Internal/Tools/Hashdict.elm diff --git a/elm.json b/elm.json index 322c34d..d1d37a0 100644 --- a/elm.json +++ b/elm.json @@ -6,6 +6,7 @@ "version": "1.0.0", "exposed-modules": [ "Matrix", + "Internal.Tools.Hashdict", "Internal.Tools.Iddict" ], "elm-version": "0.19.0 <= v < 0.20.0", diff --git a/src/Internal/Tools/Hashdict.elm b/src/Internal/Tools/Hashdict.elm new file mode 100644 index 0000000..e9ff0fc --- /dev/null +++ b/src/Internal/Tools/Hashdict.elm @@ -0,0 +1,228 @@ +module Internal.Tools.Hashdict exposing (Hashdict, decoder, empty, encode, fromList, get, insert, isEmpty, keys, member, memberKey, rehash, remove, removeKey, singleton, size, softDecoder, toList, union, values) + +{-| This module abstracts the `Dict` type with one function that assigns a +unique identifier for each value based on a function that assigns each value. + +This allows you to store values based on an externally defined identifier. + +## Dictionaries + +@docs Hashdict + +## Build + +@docs empty, singleton, insert, remove, removeKey + +## Query + +@docs isEmpty, member, memberKey, get, size + +## Lists + +@docs keys, values, toList, fromList + +## Transform + +@docs rehash, union + +## JSON coders + +@docs encode, decoder, softDecoder +-} + +import FastDict as Dict exposing (Dict) +import Json.Decode as D +import Json.Encode as E + +{-| A dictionary of keys and values where each key is defined by its value. For +example, this can be useful when every user is identifiable by their username: + + import Hashdict exposing (Hashdict) + + users : Hashdict User + users = + Hashdict.fromList .name + [ User "Alice" 28 1.65 + , User "Bob" 19 1.82 + , User "Chuck" 33 1.75 + ] + + type alias User = + { name : String + , age : Int + , height : Float + } + +In the example listed above, the users are stored by their username, which means +that all you need to know is the value "Alice" to retrieve all the information +about them. Additionally, you do not need to specify a key to insert the values. +-} +type Hashdict a + = Hashdict + { hash : a -> String + , values : Dict String a + } + +{-| Decode a hashdict from a JSON value. To create a hashdict, you are expected +to insert a hash function. If the hash function doesn't properly hash the values +as expected, the decoder will fail to decode the hashdict. +-} +decoder : (a -> String) -> D.Decoder a -> D.Decoder (Hashdict a) +decoder f xDecoder = + D.keyValuePairs xDecoder + |> D.andThen + (\items -> + if List.all (\(hash, value) -> f value == hash) items then + items + |> Dict.fromList + |> (\d -> { hash = f, values = d }) + |> Hashdict + |> D.succeed + else + D.fail "Hash function fails to properly hash all values" + ) + +{-| Create an empty hashdict. +-} +empty : (a -> String) -> Hashdict a +empty hash = + Hashdict { hash = hash, values = Dict.empty } + +{-| Encode a Hashdict into a JSON value. Keep in mind that an Elm function +cannot be universally converted to JSON, so it is up to you to preserve that +hash function! +-} +encode : (a -> E.Value) -> Hashdict a -> E.Value +encode encodeX (Hashdict h) = + h.values + |> Dict.toList + |> List.map (Tuple.mapSecond encodeX) + |> E.object + +{-| Convert an association list into a hashdict. +-} +fromList : (a -> String) -> List a -> Hashdict a +fromList hash xs = + Hashdict + { hash = hash + , values = + xs + |> List.map (\x -> ( hash x, x )) + |> Dict.fromList + } + +{-| Get the value associated with a hash. If the hash is not found, return +`Nothing`. This is useful when you are not sure if a hash will be in the +hashdict. +-} +get : String -> Hashdict a -> Maybe a +get k (Hashdict h) = + Dict.get k h.values + +{-| Insert a value into a hashdict. The key is automatically generated by the +hash function. If the function generates a collision, it replaces the existing +value in the hashdict. +-} +insert : a -> Hashdict a -> Hashdict a +insert v (Hashdict h) = + Hashdict { h | values = Dict.insert (h.hash v) v h.values } + +{-| Determine if a hashdict is empty. +-} +isEmpty : Hashdict a -> Bool +isEmpty (Hashdict h) = + Dict.isEmpty h.values + +{-| Get all of the hashes in a hashdict, sorted from lowest to highest. +-} +keys : Hashdict a -> List String +keys (Hashdict h) = + Dict.keys h.values + +{-| Determine if a value's hash is in a hashdict. +-} +member : a -> Hashdict a -> Bool +member value (Hashdict h) = + Dict.member (h.hash value) h.values + +{-| Determine if a hash is in a hashdict. +-} +memberKey : String -> Hashdict a -> Bool +memberKey key (Hashdict h) = + Dict.member key h.values + +{-| Remap a hashdict using a new hashing algorithm. +-} +rehash : (a -> String) -> Hashdict a -> Hashdict a +rehash f (Hashdict h) = + Hashdict + { hash = f + , values = + h.values + |> Dict.values + |> List.map (\v -> ( f v, v )) + |> Dict.fromList + } + +{-| Remove a value from a hashdict. If the value's hash is found, the key-value +pair is removed. If the value's hash is not found, no changes are made. + + hdict |> Hashdict.remove (User "Alice" 19 1.82) +-} +remove : a -> Hashdict a -> Hashdict a +remove v (Hashdict h) = + Hashdict { h | values = Dict.remove (h.hash v) h.values } + +{-| Remove a key from a hashdict. If the key is not found, no changes are made. + + hdict |> Hashdict.removeKey "Alice" +-} +removeKey : String -> Hashdict a -> Hashdict a +removeKey k (Hashdict h) = + Hashdict { h | values = Dict.remove k h.values } + +{-| Create a hashdict with a single key-value pair. +-} +singleton : (a -> String) -> a -> Hashdict a +singleton f v = + empty f |> insert v + +{-| Determine the number of values in a hashdict. +-} +size : Hashdict a -> Int +size (Hashdict h) = + Dict.size h.values + +{-| Decode a hashdict from a JSON value. If you cannot deduce the originally +used hash function, (or if you simply do not care) you can use this function to +decode and rehash the Hashdict using your new hash function. +-} +softDecoder : (a -> String) -> D.Decoder a -> D.Decoder (Hashdict a) +softDecoder f xDecoder = + D.keyValuePairs xDecoder + |> D.map (List.map Tuple.second >> fromList f) + +{-| Convert a hashdict into an association list of key-value pairs, sorted by +keys. +-} +toList : Hashdict a -> List (String, a) +toList (Hashdict h) = + Dict.toList h.values + +{-| Combine two hashdicts under the hash function of the first. If there is a +collision, preference is given to the first hashdict. +-} +union : Hashdict a -> Hashdict a -> Hashdict a +union (Hashdict h1) hd2 = + case rehash h1.hash hd2 of + Hashdict h2 -> + Hashdict + { hash = h1.hash + , values = Dict.union h1.values h2.values + } + +{-| Get all values stored in the hashdict, in the order of their keys. +-} +values : Hashdict a -> List a +values (Hashdict h) = + Dict.values h.values