elm-matrix-sdk-beta/src/Internal/Tools/Mashdict.elm

301 lines
7.7 KiB
Elm

module Internal.Tools.Mashdict exposing
( Mashdict
, empty, singleton, insert, remove, removeKey
, isEmpty, member, memberKey, get, size, isEqual
, keys, values, toList, fromList
, rehash, union
, encode, decoder, softDecoder
)
{-|
# Mashdict
A **mashdict**, (short for "maybe mashdict") is a hashdict that uses a hash
function that _maybe_ returns a value. In this case, the mashdict exclusively
stores values for which the hashing algorithm returns a value, and it ignores
the outcome for all other scenarios.
In general, you are advised to learn more about the
[Hashdict](Internal-Tools-Hashdict) before delving into the Mashdict.
## Dictionaries
@docs Mashdict
## Build
@docs empty, singleton, insert, remove, removeKey
## Query
@docs isEmpty, member, memberKey, get, size, isEqual
## 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, but
a value is not always given. For example, this can be relevant when not all
inserted values are relevant:
import Mashdict exposing (Mashdict)
users : Mashdict Event
users =
Mashdict.fromList .location
[ Event "Graduation party" 8 (Just "park")
, Event "National holiday" 17 Nothing
, Event "Local fair" 11 (Just "town square")
]
-- National holiday will be ignored
-- because it does not hash
type alias Event =
{ name : String
, participants : Int
, location : Maybe String
}
In the example listed above, all events are stored by their specified location,
which means that all you need to know is the value "park" to retrieve all the
information about the event at the park. As a result of optimization, this means
all values without a hash, are filtered out, as we can never query them.
-}
type Mashdict a
= Mashdict
{ hash : a -> Maybe String
, values : Dict String a
}
{-| Decode a mashdict from a JSON value. To create a mashdict, 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 mashdict.
-}
decoder : (a -> Maybe String) -> D.Decoder a -> D.Decoder (Mashdict a)
decoder f xDecoder =
D.keyValuePairs xDecoder
|> D.andThen
(\items ->
if List.all (\( hash, value ) -> f value == Just hash) items then
items
|> Dict.fromList
|> (\d -> { hash = f, values = d })
|> Mashdict
|> D.succeed
else
D.fail "Hash function fails to properly hash all values"
)
{-| Create an empty mashdict.
-}
empty : (a -> Maybe String) -> Mashdict a
empty hash =
Mashdict { hash = hash, values = Dict.empty }
{-| Encode a Mashdict 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) -> Mashdict a -> E.Value
encode encodeX (Mashdict h) =
h.values
|> Dict.toList
|> List.map (Tuple.mapSecond encodeX)
|> E.object
{-| Convert an association list into a mashdict.
-}
fromList : (a -> Maybe String) -> List a -> Mashdict a
fromList hash xs =
Mashdict
{ hash = hash
, values =
xs
|> List.filterMap (\x -> hash x |> Maybe.map (\hx -> ( hx, 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
mashdict.
-}
get : String -> Mashdict a -> Maybe a
get k (Mashdict h) =
Dict.get k h.values
{-| Insert a value into a mashdict. The key is automatically generated by the
hash function. If the function generates a collision, it replaces the existing
value in the mashdict. If the function returns `Nothing`, the value isn't
inserted and the original Mashdict is returned.
-}
insert : a -> Mashdict a -> Mashdict a
insert v (Mashdict h) =
case h.hash v of
Just hash ->
Mashdict { h | values = Dict.insert hash v h.values }
Nothing ->
Mashdict h
{-| Determine if a mashdict is empty.
-}
isEmpty : Mashdict a -> Bool
isEmpty (Mashdict h) =
Dict.isEmpty h.values
{-| Since the Hashdict contains a hash function, the == operator does not work
simply. Instead, you should use the isEqual operator.
-}
isEqual : Mashdict a -> Mashdict a -> Bool
isEqual h1 h2 =
toList h1 == toList h2
{-| Get all of the hashes in a mashdict, sorted from lowest to highest.
-}
keys : Mashdict a -> List String
keys (Mashdict h) =
Dict.keys h.values
{-| Determine if a value's hash is in a mashdict.
-}
member : a -> Mashdict a -> Bool
member value (Mashdict h) =
h.hash value
|> Maybe.map (\key -> Dict.member key h.values)
|> Maybe.withDefault False
{-| Determine if a hash is in a mashdict.
-}
memberKey : String -> Mashdict a -> Bool
memberKey key (Mashdict h) =
Dict.member key h.values
{-| Remap a mashdict using a new hashing algorithm.
-}
rehash : (a -> Maybe String) -> Mashdict a -> Mashdict a
rehash f (Mashdict h) =
Mashdict
{ hash = f
, values =
h.values
|> Dict.values
|> List.filterMap
(\v -> Maybe.map (\hash -> ( hash, v )) (f v))
|> Dict.fromList
}
{-| Remove a value from a mashdict. 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 |> Mashdict.remove (Event "Graduation party" 8 (Just "park"))
-}
remove : a -> Mashdict a -> Mashdict a
remove v (Mashdict h) =
case h.hash v of
Just hash ->
Mashdict { h | values = Dict.remove hash h.values }
Nothing ->
Mashdict h
{-| Remove a key from a mashdict. If the key is not found, no changes are made.
hdict |> Mashdict.removeKey "park"
-}
removeKey : String -> Mashdict a -> Mashdict a
removeKey k (Mashdict h) =
Mashdict { h | values = Dict.remove k h.values }
{-| Create a mashdict with a single key-value pair.
-}
singleton : (a -> Maybe String) -> a -> Mashdict a
singleton f v =
empty f |> insert v
{-| Determine the number of values in a mashdict.
-}
size : Mashdict a -> Int
size (Mashdict h) =
Dict.size h.values
{-| Decode a mashdict 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 Mashdict using your new hash function.
-}
softDecoder : (a -> Maybe String) -> D.Decoder a -> D.Decoder (Mashdict a)
softDecoder f xDecoder =
D.keyValuePairs xDecoder
|> D.map (List.map Tuple.second >> fromList f)
{-| Convert a mashdict into an association list of key-value pairs, sorted by
keys.
-}
toList : Mashdict a -> List ( String, a )
toList (Mashdict h) =
Dict.toList h.values
{-| Combine two mashdicts under the hash function of the first. If there is a
collision, preference is given to the first mashdict.
-}
union : Mashdict a -> Mashdict a -> Mashdict a
union (Mashdict h1) hd2 =
case rehash h1.hash hd2 of
Mashdict h2 ->
Mashdict
{ hash = h1.hash
, values = Dict.union h1.values h2.values
}
{-| Get all values stored in the mashdict, in the order of their keys.
-}
values : Mashdict a -> List a
values (Mashdict h) =
Dict.values h.values