unordered-containers-0.2.19.0: tests/Properties/HashMapLazy.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} -- because of Arbitrary (HashMap k v)
-- | Tests for the 'Data.HashMap.Lazy' module. We test functions by
-- comparing them to @Map@ from @containers@.
#if defined(STRICT)
#define MODULE_NAME Properties.HashMapStrict
#else
#define MODULE_NAME Properties.HashMapLazy
#endif
module MODULE_NAME (tests) where
import Control.Applicative (Const (..))
import Control.Monad (guard)
import Data.Bifoldable
import Data.Function (on)
import Data.Functor.Identity (Identity (..))
import Data.Hashable (Hashable (hashWithSalt))
import Data.Ord (comparing)
import Test.QuickCheck (Arbitrary (..), Property, elements, forAll,
(===), (==>))
import Test.QuickCheck.Function (Fun, apply)
import Test.QuickCheck.Poly (A, B)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck (testProperty)
import qualified Data.Foldable as Foldable
import qualified Data.List as List
#if defined(STRICT)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M
#else
import Data.HashMap.Lazy (HashMap)
import qualified Data.HashMap.Lazy as HM
import qualified Data.Map.Lazy as M
#endif
-- Key type that generates more hash collisions.
newtype Key = K { unK :: Int }
deriving (Arbitrary, Eq, Ord, Read, Show, Num)
instance Hashable Key where
hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 20
instance (Eq k, Hashable k, Arbitrary k, Arbitrary v) => Arbitrary (HashMap k v) where
arbitrary = fmap (HM.fromList) arbitrary
------------------------------------------------------------------------
-- * Properties
------------------------------------------------------------------------
-- ** Instances
pEq :: [(Key, Int)] -> [(Key, Int)] -> Bool
pEq xs = (M.fromList xs ==) `eq` (HM.fromList xs ==)
pNeq :: [(Key, Int)] -> [(Key, Int)] -> Bool
pNeq xs = (M.fromList xs /=) `eq` (HM.fromList xs /=)
-- We cannot compare to `Data.Map` as ordering is different.
pOrd1 :: [(Key, Int)] -> Bool
pOrd1 xs = compare x x == EQ
where
x = HM.fromList xs
pOrd2 :: [(Key, Int)] -> [(Key, Int)] -> [(Key, Int)] -> Bool
pOrd2 xs ys zs = case (compare x y, compare y z) of
(EQ, o) -> compare x z == o
(o, EQ) -> compare x z == o
(LT, LT) -> compare x z == LT
(GT, GT) -> compare x z == GT
(LT, GT) -> True -- ys greater than xs and zs.
(GT, LT) -> True
where
x = HM.fromList xs
y = HM.fromList ys
z = HM.fromList zs
pOrd3 :: [(Key, Int)] -> [(Key, Int)] -> Bool
pOrd3 xs ys = case (compare x y, compare y x) of
(EQ, EQ) -> True
(LT, GT) -> True
(GT, LT) -> True
_ -> False
where
x = HM.fromList xs
y = HM.fromList ys
pOrdEq :: [(Key, Int)] -> [(Key, Int)] -> Bool
pOrdEq xs ys = case (compare x y, x == y) of
(EQ, True) -> True
(LT, False) -> True
(GT, False) -> True
_ -> False
where
x = HM.fromList xs
y = HM.fromList ys
pReadShow :: [(Key, Int)] -> Bool
pReadShow xs = M.fromList xs == read (show (M.fromList xs))
pFunctor :: [(Key, Int)] -> Bool
pFunctor = fmap (+ 1) `eq_` fmap (+ 1)
pFoldable :: [(Int, Int)] -> Bool
pFoldable = (List.sort . Foldable.foldr (:) []) `eq`
(List.sort . Foldable.foldr (:) [])
pHashable :: [(Key, Int)] -> [Int] -> Int -> Property
pHashable xs is salt =
x == y ==> hashWithSalt salt x === hashWithSalt salt y
where
xs' = List.nubBy (\(k,_) (k',_) -> k == k') xs
ys = shuffle is xs'
x = HM.fromList xs'
y = HM.fromList ys
-- Shuffle the list using indexes in the second
shuffle :: [Int] -> [a] -> [a]
shuffle idxs = List.map snd
. List.sortBy (comparing fst)
. List.zip (idxs ++ [List.maximum (0:is) + 1 ..])
------------------------------------------------------------------------
-- ** Basic interface
pSize :: [(Key, Int)] -> Bool
pSize = M.size `eq` HM.size
pMember :: Key -> [(Key, Int)] -> Bool
pMember k = M.member k `eq` HM.member k
pLookup :: Key -> [(Key, Int)] -> Bool
pLookup k = M.lookup k `eq` HM.lookup k
pLookupOperator :: Key -> [(Key, Int)] -> Bool
pLookupOperator k = M.lookup k `eq` (HM.!? k)
pInsert :: Key -> Int -> [(Key, Int)] -> Bool
pInsert k v = M.insert k v `eq_` HM.insert k v
pDelete :: Key -> [(Key, Int)] -> Bool
pDelete k = M.delete k `eq_` HM.delete k
newtype AlwaysCollide = AC Int
deriving (Arbitrary, Eq, Ord, Show)
instance Hashable AlwaysCollide where
hashWithSalt _ _ = 1
-- White-box test that tests the case of deleting one of two keys from
-- a map, where the keys' hash values collide.
pDeleteCollision :: AlwaysCollide -> AlwaysCollide -> AlwaysCollide -> Int
-> Property
pDeleteCollision k1 k2 k3 idx = (k1 /= k2) && (k2 /= k3) && (k1 /= k3) ==>
HM.member toKeep $ HM.delete toDelete $
HM.fromList [(k1, 1 :: Int), (k2, 2), (k3, 3)]
where
which = idx `mod` 3
toDelete
| which == 0 = k1
| which == 1 = k2
| which == 2 = k3
| otherwise = error "Impossible"
toKeep
| which == 0 = k2
| which == 1 = k3
| which == 2 = k1
| otherwise = error "Impossible"
pInsertWith :: Key -> [(Key, Int)] -> Bool
pInsertWith k = M.insertWith (+) k 1 `eq_` HM.insertWith (+) k 1
pAdjust :: Key -> [(Key, Int)] -> Bool
pAdjust k = M.adjust succ k `eq_` HM.adjust succ k
pUpdateAdjust :: Key -> [(Key, Int)] -> Bool
pUpdateAdjust k = M.update (Just . succ) k `eq_` HM.update (Just . succ) k
pUpdateDelete :: Key -> [(Key, Int)] -> Bool
pUpdateDelete k = M.update (const Nothing) k `eq_` HM.update (const Nothing) k
pAlterAdjust :: Key -> [(Key, Int)] -> Bool
pAlterAdjust k = M.alter (fmap succ) k `eq_` HM.alter (fmap succ) k
pAlterInsert :: Key -> [(Key, Int)] -> Bool
pAlterInsert k = M.alter (const $ Just 3) k `eq_` HM.alter (const $ Just 3) k
pAlterDelete :: Key -> [(Key, Int)] -> Bool
pAlterDelete k = M.alter (const Nothing) k `eq_` HM.alter (const Nothing) k
-- We choose the list functor here because we don't fuss with
-- it in alterF rules and because it has a sufficiently interesting
-- structure to have a good chance of breaking if something is wrong.
pAlterF :: Key -> Fun (Maybe A) [Maybe A] -> [(Key, A)] -> Property
pAlterF k f xs =
fmap M.toAscList (M.alterF (apply f) k (M.fromList xs))
===
fmap toAscList (HM.alterF (apply f) k (HM.fromList xs))
pAlterFAdjust :: Key -> [(Key, Int)] -> Bool
pAlterFAdjust k =
runIdentity . M.alterF (Identity . fmap succ) k `eq_`
runIdentity . HM.alterF (Identity . fmap succ) k
pAlterFInsert :: Key -> [(Key, Int)] -> Bool
pAlterFInsert k =
runIdentity . M.alterF (const . Identity . Just $ 3) k `eq_`
runIdentity . HM.alterF (const . Identity . Just $ 3) k
pAlterFInsertWith :: Key -> Fun Int Int -> [(Key, Int)] -> Bool
pAlterFInsertWith k f =
runIdentity . M.alterF (Identity . Just . maybe 3 (apply f)) k `eq_`
runIdentity . HM.alterF (Identity . Just . maybe 3 (apply f)) k
pAlterFDelete :: Key -> [(Key, Int)] -> Bool
pAlterFDelete k =
runIdentity . M.alterF (const (Identity Nothing)) k `eq_`
runIdentity . HM.alterF (const (Identity Nothing)) k
pAlterFLookup :: Key
-> Fun (Maybe A) B
-> [(Key, A)] -> Bool
pAlterFLookup k f =
getConst . M.alterF (Const . apply f :: Maybe A -> Const B (Maybe A)) k
`eq`
getConst . HM.alterF (Const . apply f) k
pSubmap :: [(Key, Int)] -> [(Key, Int)] -> Bool
pSubmap xs ys = M.isSubmapOf (M.fromList xs) (M.fromList ys) ==
HM.isSubmapOf (HM.fromList xs) (HM.fromList ys)
pSubmapReflexive :: HashMap Key Int -> Bool
pSubmapReflexive m = HM.isSubmapOf m m
pSubmapUnion :: HashMap Key Int -> HashMap Key Int -> Bool
pSubmapUnion m1 m2 = HM.isSubmapOf m1 (HM.union m1 m2)
pNotSubmapUnion :: HashMap Key Int -> HashMap Key Int -> Property
pNotSubmapUnion m1 m2 = not (HM.isSubmapOf m1 m2) ==> HM.isSubmapOf m1 (HM.union m1 m2)
pSubmapDifference :: HashMap Key Int -> HashMap Key Int -> Bool
pSubmapDifference m1 m2 = HM.isSubmapOf (HM.difference m1 m2) m1
pNotSubmapDifference :: HashMap Key Int -> HashMap Key Int -> Property
pNotSubmapDifference m1 m2 =
not (HM.null (HM.intersection m1 m2)) ==>
not (HM.isSubmapOf m1 (HM.difference m1 m2))
pSubmapDelete :: HashMap Key Int -> Property
pSubmapDelete m = not (HM.null m) ==>
forAll (elements (HM.keys m)) $ \k ->
HM.isSubmapOf (HM.delete k m) m
pNotSubmapDelete :: HashMap Key Int -> Property
pNotSubmapDelete m =
not (HM.null m) ==>
forAll (elements (HM.keys m)) $ \k ->
not (HM.isSubmapOf m (HM.delete k m))
pSubmapInsert :: Key -> Int -> HashMap Key Int -> Property
pSubmapInsert k v m = not (HM.member k m) ==> HM.isSubmapOf m (HM.insert k v m)
pNotSubmapInsert :: Key -> Int -> HashMap Key Int -> Property
pNotSubmapInsert k v m = not (HM.member k m) ==> not (HM.isSubmapOf (HM.insert k v m) m)
------------------------------------------------------------------------
-- ** Combine
pUnion :: [(Key, Int)] -> [(Key, Int)] -> Bool
pUnion xs ys = M.union (M.fromList xs) `eq_` HM.union (HM.fromList xs) $ ys
pUnionWith :: [(Key, Int)] -> [(Key, Int)] -> Bool
pUnionWith xs ys = M.unionWith (-) (M.fromList xs) `eq_`
HM.unionWith (-) (HM.fromList xs) $ ys
pUnionWithKey :: [(Key, Int)] -> [(Key, Int)] -> Bool
pUnionWithKey xs ys = M.unionWithKey go (M.fromList xs) `eq_`
HM.unionWithKey go (HM.fromList xs) $ ys
where
go :: Key -> Int -> Int -> Int
go (K k) i1 i2 = k - i1 + i2
pUnions :: [[(Key, Int)]] -> Bool
pUnions xss = M.toAscList (M.unions (map M.fromList xss)) ==
toAscList (HM.unions (map HM.fromList xss))
------------------------------------------------------------------------
-- ** Transformations
pMap :: [(Key, Int)] -> Bool
pMap = M.map (+ 1) `eq_` HM.map (+ 1)
pTraverse :: [(Key, Int)] -> Bool
pTraverse xs =
List.sort (fmap (List.sort . M.toList) (M.traverseWithKey (\_ v -> [v + 1, v + 2]) (M.fromList (take 10 xs))))
== List.sort (fmap (List.sort . HM.toList) (HM.traverseWithKey (\_ v -> [v + 1, v + 2]) (HM.fromList (take 10 xs))))
pMapKeys :: [(Int, Int)] -> Bool
pMapKeys = M.mapKeys (+1) `eq_` HM.mapKeys (+1)
------------------------------------------------------------------------
-- ** Difference and intersection
pDifference :: [(Key, Int)] -> [(Key, Int)] -> Bool
pDifference xs ys = M.difference (M.fromList xs) `eq_`
HM.difference (HM.fromList xs) $ ys
pDifferenceWith :: [(Key, Int)] -> [(Key, Int)] -> Bool
pDifferenceWith xs ys = M.differenceWith f (M.fromList xs) `eq_`
HM.differenceWith f (HM.fromList xs) $ ys
where
f x y = if x == 0 then Nothing else Just (x - y)
pIntersection :: [(Key, Int)] -> [(Key, Int)] -> Bool
pIntersection xs ys =
M.intersection (M.fromList xs)
`eq_` HM.intersection (HM.fromList xs)
$ ys
pIntersectionWith :: [(Key, Int)] -> [(Key, Int)] -> Bool
pIntersectionWith xs ys = M.intersectionWith (-) (M.fromList xs) `eq_`
HM.intersectionWith (-) (HM.fromList xs) $ ys
pIntersectionWithKey :: [(Key, Int)] -> [(Key, Int)] -> Bool
pIntersectionWithKey xs ys = M.intersectionWithKey go (M.fromList xs) `eq_`
HM.intersectionWithKey go (HM.fromList xs) $ ys
where
go :: Key -> Int -> Int -> Int
go (K k) i1 i2 = k - i1 - i2
------------------------------------------------------------------------
-- ** Folds
pFoldr :: [(Int, Int)] -> Bool
pFoldr = (List.sort . M.foldr (:) []) `eq` (List.sort . HM.foldr (:) [])
pFoldl :: [(Int, Int)] -> Bool
pFoldl = (List.sort . M.foldl (flip (:)) []) `eq` (List.sort . HM.foldl (flip (:)) [])
pBifoldMap :: [(Int, Int)] -> Bool
pBifoldMap xs = concatMap f (HM.toList m) == bifoldMap (:[]) (:[]) m
where f (k, v) = [k, v]
m = HM.fromList xs
pBifoldr :: [(Int, Int)] -> Bool
pBifoldr xs = concatMap f (HM.toList m) == bifoldr (:) (:) [] m
where f (k, v) = [k, v]
m = HM.fromList xs
pBifoldl :: [(Int, Int)] -> Bool
pBifoldl xs = reverse (concatMap f $ HM.toList m) == bifoldl (flip (:)) (flip (:)) [] m
where f (k, v) = [k, v]
m = HM.fromList xs
pFoldrWithKey :: [(Int, Int)] -> Bool
pFoldrWithKey = (sortByKey . M.foldrWithKey f []) `eq`
(sortByKey . HM.foldrWithKey f [])
where f k v z = (k, v) : z
pFoldMapWithKey :: [(Int, Int)] -> Bool
pFoldMapWithKey = (sortByKey . M.foldMapWithKey f) `eq`
(sortByKey . HM.foldMapWithKey f)
where f k v = [(k, v)]
pFoldrWithKey' :: [(Int, Int)] -> Bool
pFoldrWithKey' = (sortByKey . M.foldrWithKey' f []) `eq`
(sortByKey . HM.foldrWithKey' f [])
where f k v z = (k, v) : z
pFoldlWithKey :: [(Int, Int)] -> Bool
pFoldlWithKey = (sortByKey . M.foldlWithKey f []) `eq`
(sortByKey . HM.foldlWithKey f [])
where f z k v = (k, v) : z
pFoldlWithKey' :: [(Int, Int)] -> Bool
pFoldlWithKey' = (sortByKey . M.foldlWithKey' f []) `eq`
(sortByKey . HM.foldlWithKey' f [])
where f z k v = (k, v) : z
pFoldl' :: [(Int, Int)] -> Bool
pFoldl' = (List.sort . M.foldl' (flip (:)) []) `eq` (List.sort . HM.foldl' (flip (:)) [])
pFoldr' :: [(Int, Int)] -> Bool
pFoldr' = (List.sort . M.foldr' (:) []) `eq` (List.sort . HM.foldr' (:) [])
------------------------------------------------------------------------
-- ** Filter
pMapMaybeWithKey :: [(Key, Int)] -> Bool
pMapMaybeWithKey = M.mapMaybeWithKey f `eq_` HM.mapMaybeWithKey f
where f k v = guard (odd (unK k + v)) >> Just (v + 1)
pMapMaybe :: [(Key, Int)] -> Bool
pMapMaybe = M.mapMaybe f `eq_` HM.mapMaybe f
where f v = guard (odd v) >> Just (v + 1)
pFilter :: [(Key, Int)] -> Bool
pFilter = M.filter odd `eq_` HM.filter odd
pFilterWithKey :: [(Key, Int)] -> Bool
pFilterWithKey = M.filterWithKey p `eq_` HM.filterWithKey p
where p k v = odd (unK k + v)
------------------------------------------------------------------------
-- ** Conversions
-- The free magma is used to test that operations are applied in the
-- same order.
data Magma a
= Leaf a
| Op (Magma a) (Magma a)
deriving (Show, Eq, Ord)
instance Hashable a => Hashable (Magma a) where
hashWithSalt s (Leaf a) = hashWithSalt s (hashWithSalt (1::Int) a)
hashWithSalt s (Op m n) = hashWithSalt s (hashWithSalt (hashWithSalt (2::Int) m) n)
-- 'eq_' already calls fromList.
pFromList :: [(Key, Int)] -> Bool
pFromList = id `eq_` id
pFromListWith :: [(Key, Int)] -> Bool
pFromListWith kvs = (M.toAscList $ M.fromListWith Op kvsM) ==
(toAscList $ HM.fromListWith Op kvsM)
where kvsM = fmap (fmap Leaf) kvs
pFromListWithKey :: [(Key, Int)] -> Bool
pFromListWithKey kvs = (M.toAscList $ M.fromListWithKey combine kvsM) ==
(toAscList $ HM.fromListWithKey combine kvsM)
where kvsM = fmap (\(K k,v) -> (Leaf k, Leaf v)) kvs
combine k v1 v2 = Op k (Op v1 v2)
pToList :: [(Key, Int)] -> Bool
pToList = M.toAscList `eq` toAscList
pElems :: [(Key, Int)] -> Bool
pElems = (List.sort . M.elems) `eq` (List.sort . HM.elems)
pKeys :: [(Key, Int)] -> Bool
pKeys = (List.sort . M.keys) `eq` (List.sort . HM.keys)
------------------------------------------------------------------------
-- * Test list
tests :: TestTree
tests =
testGroup
#if defined(STRICT)
"Data.HashMap.Strict"
#else
"Data.HashMap.Lazy"
#endif
[
-- Instances
testGroup "instances"
[ testProperty "==" pEq
, testProperty "/=" pNeq
, testProperty "compare reflexive" pOrd1
, testProperty "compare transitive" pOrd2
, testProperty "compare antisymmetric" pOrd3
, testProperty "Ord => Eq" pOrdEq
, testProperty "Read/Show" pReadShow
, testProperty "Functor" pFunctor
, testProperty "Foldable" pFoldable
, testProperty "Hashable" pHashable
]
-- Basic interface
, testGroup "basic interface"
[ testProperty "size" pSize
, testProperty "member" pMember
, testProperty "lookup" pLookup
, testProperty "!?" pLookupOperator
, testProperty "insert" pInsert
, testProperty "delete" pDelete
, testProperty "deleteCollision" pDeleteCollision
, testProperty "insertWith" pInsertWith
, testProperty "adjust" pAdjust
, testProperty "updateAdjust" pUpdateAdjust
, testProperty "updateDelete" pUpdateDelete
, testProperty "alterAdjust" pAlterAdjust
, testProperty "alterInsert" pAlterInsert
, testProperty "alterDelete" pAlterDelete
, testProperty "alterF" pAlterF
, testProperty "alterFAdjust" pAlterFAdjust
, testProperty "alterFInsert" pAlterFInsert
, testProperty "alterFInsertWith" pAlterFInsertWith
, testProperty "alterFDelete" pAlterFDelete
, testProperty "alterFLookup" pAlterFLookup
, testGroup "isSubmapOf"
[ testProperty "container compatibility" pSubmap
, testProperty "m ⊆ m" pSubmapReflexive
, testProperty "m1 ⊆ m1 ∪ m2" pSubmapUnion
, testProperty "m1 ⊈ m2 ⇒ m1 ∪ m2 ⊈ m1" pNotSubmapUnion
, testProperty "m1\\m2 ⊆ m1" pSubmapDifference
, testProperty "m1 ∩ m2 ≠ ∅ ⇒ m1 ⊈ m1\\m2 " pNotSubmapDifference
, testProperty "delete k m ⊆ m" pSubmapDelete
, testProperty "m ⊈ delete k m " pNotSubmapDelete
, testProperty "k ∉ m ⇒ m ⊆ insert k v m" pSubmapInsert
, testProperty "k ∉ m ⇒ insert k v m ⊈ m" pNotSubmapInsert
]
]
-- Combine
, testProperty "union" pUnion
, testProperty "unionWith" pUnionWith
, testProperty "unionWithKey" pUnionWithKey
, testProperty "unions" pUnions
-- Transformations
, testProperty "map" pMap
, testProperty "traverse" pTraverse
, testProperty "mapKeys" pMapKeys
-- Folds
, testGroup "folds"
[ testProperty "foldr" pFoldr
, testProperty "foldl" pFoldl
, testProperty "bifoldMap" pBifoldMap
, testProperty "bifoldr" pBifoldr
, testProperty "bifoldl" pBifoldl
, testProperty "foldrWithKey" pFoldrWithKey
, testProperty "foldlWithKey" pFoldlWithKey
, testProperty "foldrWithKey'" pFoldrWithKey'
, testProperty "foldlWithKey'" pFoldlWithKey'
, testProperty "foldl'" pFoldl'
, testProperty "foldr'" pFoldr'
, testProperty "foldMapWithKey" pFoldMapWithKey
]
, testGroup "difference and intersection"
[ testProperty "difference" pDifference
, testProperty "differenceWith" pDifferenceWith
, testProperty "intersection" pIntersection
, testProperty "intersectionWith" pIntersectionWith
, testProperty "intersectionWithKey" pIntersectionWithKey
]
-- Filter
, testGroup "filter"
[ testProperty "filter" pFilter
, testProperty "filterWithKey" pFilterWithKey
, testProperty "mapMaybe" pMapMaybe
, testProperty "mapMaybeWithKey" pMapMaybeWithKey
]
-- Conversions
, testGroup "conversions"
[ testProperty "elems" pElems
, testProperty "keys" pKeys
, testProperty "fromList" pFromList
, testProperty "fromListWith" pFromListWith
, testProperty "fromListWithKey" pFromListWithKey
, testProperty "toList" pToList
]
]
------------------------------------------------------------------------
-- * Model
type Model k v = M.Map k v
-- | Check that a function operating on a 'HashMap' is equivalent to
-- one operating on a 'Model'.
eq :: (Eq a, Eq k, Hashable k, Ord k)
=> (Model k v -> a) -- ^ Function that modifies a 'Model'
-> (HM.HashMap k v -> a) -- ^ Function that modified a 'HashMap' in the same
-- way
-> [(k, v)] -- ^ Initial content of the 'HashMap' and 'Model'
-> Bool -- ^ True if the functions are equivalent
eq f g xs = g (HM.fromList xs) == f (M.fromList xs)
infix 4 `eq`
eq_ :: (Eq k, Eq v, Hashable k, Ord k)
=> (Model k v -> Model k v) -- ^ Function that modifies a 'Model'
-> (HM.HashMap k v -> HM.HashMap k v) -- ^ Function that modified a
-- 'HashMap' in the same way
-> [(k, v)] -- ^ Initial content of the 'HashMap'
-- and 'Model'
-> Bool -- ^ True if the functions are
-- equivalent
eq_ f g = (M.toAscList . f) `eq` (toAscList . g)
infix 4 `eq_`
------------------------------------------------------------------------
-- * Helpers
sortByKey :: Ord k => [(k, v)] -> [(k, v)]
sortByKey = List.sortBy (compare `on` fst)
toAscList :: Ord k => HM.HashMap k v -> [(k, v)]
toAscList = List.sortBy (compare `on` fst) . HM.toList