packages feed

expiring-cache-map (empty) → 0.0.5.0

raw patch · 17 files changed

+1208/−0 lines, 17 filesdep +basedep +bytestringdep +containerssetup-changed

Dependencies added: base, bytestring, containers, hashable, time, unordered-containers

Files

+ Caching/ExpiringCacheMap/HashECM.hs view
@@ -0,0 +1,209 @@+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module : Caching.ExpiringCacheMap.HashECM
+-- Copyright: (c) 2014 Edward L. Blake
+-- License: BSD-style
+-- Maintainer: Edward L. Blake <edwardlblake@gmail.com>
+-- Stability: experimental
+-- Portability: portable
+--
+-- A cache that holds values for a length of time that uses 'Hashable' keys
+-- with "Data.HashMap.Strict".
+--
+-- An example of creating a cache for accessing files:
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- > module Example where
+-- > 
+-- > import Caching.ExpiringCacheMap.HashECM (newECMIO, lookupECM, CacheSettings(..), consistentDuration)
+-- > 
+-- > import qualified Data.Time.Clock.POSIX as POSIX (POSIXTime, getPOSIXTime)
+-- > import qualified Data.ByteString.Char8 as BS
+-- > import System.IO (withFile, IOMode(ReadMode))
+-- > 
+-- > example = do
+-- >   filecache <- newECMIO
+-- >         (consistentDuration 100 -- Duration between access and expiry time of each item
+-- >           (\state id -> do BS.putStrLn "Reading a file again..."
+-- >                            withFile (case id :: BS.ByteString of
+-- >                                        "file1" -> "file1.txt"
+-- >                                        "file2" -> "file2.txt")
+-- >                               ReadMode $
+-- >                               \fh -> do content <- BS.hGetContents fh
+-- >                                         return $! (state, content)))
+-- >         (do time <- POSIX.getPOSIXTime
+-- >             return (round (time * 100)))
+-- >         12000 -- Time check frequency: (accumulator `mod` this_number) == 0.
+-- >         (CacheWithLRUList
+-- >           6     -- Expected size of key-value map when removing elements.
+-- >           6     -- Size of list when to remove items from key-value map.
+-- >           12    -- Size of list when to compact
+-- >           )
+-- >   
+-- >   -- Use lookupECM whenever the contents of "file1" is needed.
+-- >   b <- lookupECM filecache "file1"
+-- >   BS.putStrLn b
+-- >   return ()
+-- > 
+-- 
+
+module Caching.ExpiringCacheMap.HashECM (
+    
+    -- * Create cache
+    newECMIO,
+    newECMForM,
+    consistentDuration,
+    
+    -- * Request value from cache
+    lookupECM,
+    
+    -- * Type
+    ECM,
+    CacheSettings(..)
+) where
+
+import qualified Control.Concurrent.MVar as MV
+import qualified Data.HashMap.Strict as HM
+import qualified Data.List as L
+import Data.Hashable (Hashable(..))
+
+import Caching.ExpiringCacheMap.Internal.Internal (updateUses, detECM)
+import Caching.ExpiringCacheMap.Types
+import Caching.ExpiringCacheMap.Internal.Types
+
+-- | Create a new expiring cache for retrieving uncached values via 'IO'
+-- interaction (such as in the case of reading a file from disk), with 
+-- a shared state lock via an 'MV.MVar' to manage cache state.
+-- 
+newECMIO :: (Eq k, Hashable k) => (Maybe s -> k -> IO (TimeUnits, (Maybe s, v))) -> (IO TimeUnits)
+  -> ECMIncr 
+  -> CacheSettings
+    -> IO (ECM IO MV.MVar s HM.HashMap k v)
+newECMIO retr gettime timecheckmodulo cachesettings = do
+  newECMForM retr gettime timecheckmodulo cachesettings
+    MV.newMVar MV.modifyMVar MV.readMVar
+  
+-- | Create a new expiring cache along arbitrary monads with provided
+-- functions to create cache state in 'Monad' m2, and modify and read
+-- cache state in 'Monad' m1.
+--
+newECMForM :: (Monad m1, Monad m2) => (Eq k, Hashable k) => (Maybe s -> k -> m1 (TimeUnits, (Maybe s, v))) -> (m1 TimeUnits)
+  -> ECMIncr 
+  -> CacheSettings
+  -> ECMNewState m2 mv s HM.HashMap k v
+  -> ECMEnterState m1 mv s HM.HashMap k v
+  -> ECMReadState m1 mv s HM.HashMap k v
+    -> m2 (ECM m1 mv s HM.HashMap k v)
+newECMForM retr gettime timecheckmodulo (CacheWithLRUList minimumkeep removalsize compactlistsize)
+           newstate enterstate readstate = do
+  m'maps <- newstate $ CacheState ( Nothing, HM.empty, ([], 0), 0 )
+  return $ ECM ( m'maps, retr, gettime, minimumkeep, 
+                 timecheckmodulo, removalsize, compactlistsize,
+                 enterstate, readstate )
+
+
+-- | Request a value associated with a key from the cache.
+--
+--  * If the value is not in the cache, the value will be requested through the 
+--    function defined when the 'ECM' value was created, its computation
+--    returned and the value stored in the cache state map.
+-- 
+--  * If the value is in the cache and has not expired, it will be returned.
+-- 
+--  * If the value is in the cache and a new time is computed in the same
+--    lookup, and the value has been determined to have since expired, it 
+--    will be discarded and a new value will be requested for this computation.
+--
+-- Every 'lookupECM' computation increments an accumulator in the cache state
+-- which is used to keep track of the succession of key accesses. This history 
+-- of key accesses is then used to remove entries from the cache back down to 
+-- a minimum size. Also, when the modulo of the accumulator and the modulo 
+-- value computes to 0, the time request function defined when the 'ECM' value
+-- was created is invoked for the current time to determine of which if any of
+-- the entries in the cache state map needs to be removed. In some cases the 
+-- accumulator may get incremented more than once in a 'lookupECM' computation.
+--
+-- As the accumulator is a bound unsigned integer, when the accumulator
+-- increments back to 0, the cache state is completely cleared.
+-- 
+lookupECM :: (Monad m, Eq k, Hashable k) => ECM m mv s HM.HashMap k v -> k -> m v
+lookupECM ecm id = do
+  enter m'maps $
+    \(CacheState (retr_state, maps, uses, incr)) ->
+      let incr' = incr + 1
+       in if incr' < incr
+            -- Word incrementor has cycled back to 0,
+            -- so may as well clear the cache completely.
+            then lookupECM' (retr_state, HM.empty, ([], 0), 0) (0+1)
+            else lookupECM' (retr_state, maps, uses, incr) incr'
+  where
+  
+    ECM ( m'maps, retr, gettime, minimumkeep, timecheckmodulo, removalsize, 
+          compactlistsize, enter, _ro ) = ecm
+    
+    mnub = HM.toList . HM.fromList . reverse
+    lookupECM' (retr_state, maps, uses, incr) incr' = do
+      let uses' = updateUses uses id incr' compactlistsize mnub
+      (ret, do_again) <- det retr_state maps uses' incr'
+      if do_again
+        then do let (CacheState (retr_state', maps', uses'', incr''), _) = ret
+                    uses''' = updateUses uses'' id incr'' compactlistsize mnub
+                (ret', _) <- det retr_state' maps' uses''' incr''
+                return ret'
+        else return ret
+    
+    det retr_state maps uses' incr' =
+      detECM (HM.lookup id maps) retr_state (retr retr_state id)
+        (\time_r -> HM.insert id time_r maps)
+        (\time_r keepuses -> HM.insert id time_r $! HM.intersection maps $ HM.fromList keepuses)
+        mnub
+        gettime
+        HM.filter
+        uses' incr' timecheckmodulo maps minimumkeep removalsize
+
+
+{-
+-- This function differs from 'lookupECM' only in the case that the value
+-- being requested also causes a new time to have been computed during the 
+-- same lookup, and have been found to be out of date. When the condition 
+-- happens, this function returns the old cached value without attempting
+-- to request a new value, despite being out of date. However, it does
+-- clear the key from the key-value store for the next request.
+-- 
+lookupECMUse :: (Monad m, Eq k, Hashable k) => ECM m mv s HM.HashMap k v -> k -> m v
+lookupECMUse ecm id = do
+  enter m'maps $
+    \(CacheState (retr_state, maps, uses, incr)) ->
+      let incr' = incr + 1
+       in if incr' < incr
+            -- Word incrementor has cycled back to 0,
+            -- so may as well clear the cache completely.
+            then lookupECM' (retr_state, HM.empty, ([], 0), 0) (0+1)
+            else lookupECM' (retr_state, maps, uses, incr) incr'
+  where
+  
+    ECM ( m'maps, retr, gettime, minimumkeep, timecheckmodulo, removalsize, 
+          compactlistsize, enter, _ro ) = ecm
+    
+    mnub = HM.toList . HM.fromList . reverse
+    lookupECM' (retr_state, maps, uses, incr) incr' = do
+      let uses' = updateUses uses id incr' compactlistsize mnub
+      (ret, _) <-
+          detECM (HM.lookup id maps) retr_state (retr retr_state id)
+            (\time_r -> HM.insert id time_r maps)
+            (\time_r keepuses -> HM.insert id time_r $! HM.intersection maps $ HM.fromList keepuses)
+            mnub
+            gettime
+            HM.filter
+            uses' incr' timecheckmodulo maps minimumkeep removalsize
+      return ret
+-}
+
+
+-- | Used with 'newECMIO' or 'newECMForM' to provide a consistent duration for requested values.
+consistentDuration :: (Monad m, Eq k, Hashable k) => TimeUnits -> (Maybe s -> k -> m (Maybe s, v)) -> (Maybe s -> k -> m (TimeUnits, (Maybe s, v)))
+consistentDuration duration fun =
+  \state id -> do
+    ret <- fun state id
+    return (duration, ret)
+
+ Caching/ExpiringCacheMap/Internal/Internal.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module : Caching.ExpiringCacheMap.Internal
+-- Copyright: (c) 2014 Edward L. Blake
+-- License: BSD-style
+-- Maintainer: Edward L. Blake <edwardlblake@gmail.com>
+-- Stability: experimental
+-- Portability: portable
+--
+-- A module with internal functions used in common by HashECM and OrdECM.
+-- Assume these functions to change from version to version.
+-- 
+
+module Caching.ExpiringCacheMap.Internal.Internal (
+    updateUses,
+    detECM,
+    getStatsString
+) where
+
+import qualified Data.List as L
+
+import Caching.ExpiringCacheMap.Types
+import Caching.ExpiringCacheMap.Internal.Types
+
+updateUses :: (Eq k) => ([(k, ECMIncr)], ECMULength) -> k
+  -> ECMIncr -> ECMULength -> ([(k, ECMIncr)] -> [(k, ECMIncr)])
+    -> ([(k, ECMIncr)], ECMULength)
+{-# INLINE updateUses #-}
+updateUses uses id incr' compactlistsize compactUses =
+    case uses of
+        (((id', _) : rest), lcount) | id' == id ->
+          (((id', incr') : rest), lcount)
+        ((latest : (id', _) : rest), lcount) | id' == id ->
+          (((id', incr') : latest : rest), lcount)
+        ((latest : latest' : (id', _) : rest), lcount) | id' == id ->
+          (((id', incr') : latest : latest' : rest), lcount)
+        (usesl, lcount) ->
+          if lcount > compactlistsize
+            then let newusesl = compactUses usesl
+                  in ((id, incr') : newusesl, (L.length newusesl) + 1)
+            else ((id, incr') : usesl, lcount+1)
+
+detECM
+  :: (Monad m, Eq k) =>
+     Maybe (TimeUnits, TimeUnits, v)
+     -> Maybe s
+     -> m (TimeUnits, (Maybe s, v))
+     -> ((TimeUnits, TimeUnits, v) -> mp k (TimeUnits, TimeUnits, v))
+     -> ((TimeUnits, TimeUnits, v) -> [(k, ECMIncr)] -> mp k (TimeUnits, TimeUnits, v))
+     -> ([(k, ECMIncr)] -> [(k, ECMIncr)])
+     -> m TimeUnits
+     -> (((TimeUnits, TimeUnits, v) -> Bool)
+         -> mp k (TimeUnits, TimeUnits, v) -> mp k (TimeUnits, TimeUnits, v))
+     -> ([(k, ECMIncr)], ECMULength)
+     -> ECMIncr
+     -> ECMIncr
+     -> mp k (TimeUnits, TimeUnits, v)
+     -> ECMMapSize
+     -> ECMULength
+       -> m ((CacheState s mp k v, v), Bool)
+{-# INLINE detECM #-}
+detECM result retr_state retr_id insert_id1 insert_id2 mnub gettime filt uses' incr' timecheckmodulo maps minimumkeep removalsize = 
+    case result of
+        Nothing -> do
+          (expirytime, (retr_state', r)) <- retr_id
+          time <- gettime
+          let (newmaps,newuses) = insertAndPerhapsRemoveSome time r expirytime uses'
+          return $! ((CacheState (retr_state', newmaps, newuses, incr'), r), False)
+        Just (_accesstime, _expirytime, m) -> do
+          if incr' `mod` timecheckmodulo == 0
+            then do
+              time <- gettime
+              return ((CacheState (retr_state, filterExpired time maps, uses', incr'), m), True)
+            else return ((CacheState (retr_state, maps, uses', incr'), m), False)
+  where
+  
+    getKeepAndRemove =
+      finalTup . splitAt minimumkeep . reverse . 
+          sortI . map swap2 . mnub
+        where swap2 (a,b) = (b,a)
+              finalTup (l1,l2) = 
+                (map (\(c,k) -> (k,c)) l1, map (\(c,k) -> k) l2)
+              sortI = L.sortBy (\(l,_) (r,_) -> compare l r)
+    
+    insertAndPerhapsRemoveSome time r expirytime uses =
+      if lcount >= removalsize
+        then 
+          let (keepuses, _removekeys) = getKeepAndRemove usesl
+              newmaps = insert_id2 (time, expirytime, r) keepuses
+           in (filterExpired time newmaps, (keepuses, L.length keepuses))
+        else
+          let newmaps = insert_id1 (time, expirytime, r)
+           in (filterExpired time newmaps, uses)
+      where
+        (usesl, lcount) = uses
+    
+    filterExpired time =
+      filt (\(accesstime, expirytime, value) ->
+                 (accesstime <= time) &&
+                   (accesstime > (time - expirytime)))
+
+
+-- | Debugging function
+--
+getStatsString ecm = do
+  CacheState (_retr_state, _maps, uses, _incr) <- ro m'uses
+  return $ show uses
+  where
+    ECM ( m'uses, _retr, _gettime, _minimumkeep, _timecheckmodulo, _removalsize,
+          _compactlistsize, _enter, ro ) = ecm
+
+ Caching/ExpiringCacheMap/Internal/Types.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module : Caching.ExpiringCacheMap.Internal.Types
+-- Copyright: (c) 2014 Edward L. Blake
+-- License: BSD-style
+-- Maintainer: Edward L. Blake <edwardlblake@gmail.com>
+-- Stability: experimental
+-- Portability: portable
+--
+-- Internal types.
+-- 
+
+module Caching.ExpiringCacheMap.Internal.Types (
+    -- * Cache internals
+    ECM(..),
+    CacheState(..),
+    ECMNewState,
+    ECMEnterState,
+    ECMReadState
+) where
+
+import qualified Control.Concurrent.MVar as MV
+import Caching.ExpiringCacheMap.Utils.Types
+
+type ECMNewState a b s m k v = (CacheState s m k v) -> a (b (CacheState s m k v))
+
+type ECMEnterState a b s m k v = b (CacheState s m k v) -> ((CacheState s m k v) -> a ((CacheState s m k v), v)) -> a v
+
+type ECMReadState a b s m k v = b (CacheState s m k v) -> a (CacheState s m k v)
+
+-- | The cache state.
+newtype CacheState s m k v =
+  CacheState (Maybe s, m k (TimeUnits, TimeUnits, v), ([(k, ECMIncr)], ECMULength), ECMIncr)
+
+-- | The type that encapsulates a cache map.
+newtype ECM a b s m k v = ECM ( b (CacheState s m k v),
+                  Maybe s -> k -> a (TimeUnits, (Maybe s, v)),
+                  a TimeUnits,
+                  ECMMapSize,
+                  -- TimeUnits,
+                  ECMIncr,
+                  ECMULength,
+                  ECMULength,
+                  ECMEnterState a b s m k v,
+                  ECMReadState a b s m k v)
+ Caching/ExpiringCacheMap/OrdECM.hs view
@@ -0,0 +1,205 @@+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module : Caching.ExpiringCacheMap.OrdECM
+-- Copyright: (c) 2014 Edward L. Blake
+-- License: BSD-style
+-- Maintainer: Edward L. Blake <edwardlblake@gmail.com>
+-- Stability: experimental
+-- Portability: portable
+--
+-- A cache that holds values for a length of time that uses 'Ord' keys with 
+-- "Data.Map.Strict".
+-- 
+-- An example of creating a cache for accessing files:
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- > module Example where
+-- > 
+-- > import Caching.ExpiringCacheMap.OrdECM (newECMIO, lookupECM, CacheSettings(..), consistentDuration)
+-- > 
+-- > import qualified Data.Time.Clock.POSIX as POSIX (POSIXTime, getPOSIXTime)
+-- > import qualified Data.ByteString.Char8 as BS
+-- > import System.IO (withFile, IOMode(ReadMode))
+-- > 
+-- > example = do
+-- >   filecache <- newECMIO
+-- >         (consistentDuration 100 -- Duration between access and expiry time of each item
+-- >           (\state id -> do BS.putStrLn "Reading a file again..."
+-- >                            withFile (case id :: BS.ByteString of
+-- >                                        "file1" -> "file1.txt"
+-- >                                        "file2" -> "file2.txt")
+-- >                               ReadMode $
+-- >                               \fh -> do content <- BS.hGetContents fh
+-- >                                         return $! (state, content)))
+-- >         (do time <- POSIX.getPOSIXTime
+-- >             return (round (time * 100)))
+-- >         12000 -- Time check frequency: (accumulator `mod` this_number) == 0.
+-- >         (CacheWithLRUList
+-- >           6     -- Expected size of key-value map when removing elements.
+-- >           6     -- Size of list when to remove items from key-value map.
+-- >           12    -- Size of list when to compact
+-- >           )
+-- >   
+-- >   -- Use lookupECM whenever the contents of "file1" is needed.
+-- >   b <- lookupECM filecache "file1"
+-- >   BS.putStrLn b
+-- >   return ()
+-- > 
+--
+
+module Caching.ExpiringCacheMap.OrdECM (
+    -- * Create cache
+    newECMIO,
+    newECMForM,
+    consistentDuration,
+    
+    -- * Request value from cache
+    lookupECM,
+    
+    -- * Type
+    ECM,
+    CacheSettings(..)
+) where
+
+import qualified Control.Concurrent.MVar as MV
+import qualified Data.Map.Strict as M
+import qualified Data.List as L
+
+import Caching.ExpiringCacheMap.Internal.Internal (updateUses, detECM)
+import Caching.ExpiringCacheMap.Types
+import Caching.ExpiringCacheMap.Internal.Types
+
+-- | Create a new expiring cache for retrieving uncached values via 'IO'
+-- interaction (such as in the case of reading a file from disk), with
+-- a shared state lock via an 'MV.MVar' to manage cache state.
+--
+newECMIO :: Ord k => (Maybe s -> k -> IO (TimeUnits, (Maybe s, v))) -> (IO TimeUnits)
+  -> ECMIncr 
+  -> CacheSettings
+    -> IO (ECM IO MV.MVar s M.Map k v)
+newECMIO retr gettime timecheckmodulo settings = do
+  newECMForM retr gettime timecheckmodulo settings
+    MV.newMVar MV.modifyMVar MV.readMVar
+
+-- | Create a new expiring cache along arbitrary monads with provided
+-- functions to create cache state in 'Monad' m2, and modify and read
+-- cache state in 'Monad' m1.
+--
+newECMForM :: (Monad m1, Monad m2) => Ord k => (Maybe s -> k -> m1 (TimeUnits, (Maybe s, v))) -> (m1 TimeUnits)
+  -> ECMIncr
+  -> CacheSettings
+  -> ECMNewState m2 mv s M.Map k v
+  -> ECMEnterState m1 mv s M.Map k v
+  -> ECMReadState m1 mv s M.Map k v
+    -> m2 (ECM m1 mv s M.Map k v)
+newECMForM retr gettime timecheckmodulo (CacheWithLRUList minimumkeep removalsize compactlistsize)
+           newstate enterstate readstate = do
+  m'maps <- newstate $ CacheState ( Nothing, M.empty, ([], 0), 0 )
+  return $ ECM ( m'maps, retr, gettime, minimumkeep, timecheckmodulo, removalsize,
+                 compactlistsize, enterstate, readstate )
+
+-- | Request a value associated with a key from the cache.
+--
+--  * If the value is not in the cache, it will be requested through the 
+--    function defined through 'newECM', its computation returned and the
+--    value stored in the cache state map.
+-- 
+--  * If the value is in the cache and has not expired, it will be returned.
+--
+--  * If the value is in the cache and a new time is computed in the same
+--    lookup, and the value has been determined to have since expired, it 
+--    will be discarded and a new value will be requested for this computation.
+--
+-- Every 'lookupECM' computation increments an accumulator in the cache state 
+-- which is used to keep track of the succession of key accesses. This history 
+-- of key accesses is then used to remove entries from the cache back down to 
+-- a minimum size. Also, when the modulo of the accumulator and the modulo 
+-- value computes to 0, the time request function defined when the 'ECM' value
+-- was created is invoked for the current time to determine of which if any of
+-- the entries in the cache state map needs to be removed. In some cases the 
+-- accumulator may get incremented more than once in a 'lookupECM' computation.
+--
+-- As the accumulator is a bound unsigned integer, when the accumulator
+-- increments back to 0, the cache state is completely cleared.
+-- 
+lookupECM :: (Monad m, Ord k) => ECM m mv s M.Map k v -> k -> m v
+lookupECM ecm id = do
+  enter m'maps $
+    \(CacheState (retr_state, maps, uses, incr)) ->
+      let incr' = incr + 1
+       in if incr' < incr
+            -- Word incrementor has cycled back to 0,
+            -- so may as well clear the cache completely.
+            then lookupECM' (retr_state, M.empty, ([], 0), 0) (0+1)
+            else lookupECM' (retr_state, maps, uses, incr) incr'
+  where
+    
+    ECM ( m'maps, retr, gettime, minimumkeep, timecheckmodulo, removalsize,
+          compactlistsize, enter, _ro ) = ecm
+  
+    mnub = M.toList . M.fromList . reverse
+    lookupECM' (retr_state, maps, uses, incr) incr' = do
+      let uses' = updateUses uses id incr' compactlistsize mnub
+      (ret, do_again) <- det retr_state maps uses' incr'
+      if do_again
+        then do let (CacheState (retr_state', maps', uses'', incr''), _) = ret
+                    uses''' = updateUses uses'' id incr'' compactlistsize mnub
+                (ret', _) <- det retr_state' maps' uses''' incr''
+                return ret'
+        else return ret
+    
+    det retr_state maps uses' incr' =
+      detECM (M.lookup id maps) retr_state (retr retr_state id)
+        (\time_r -> M.insert id time_r maps)
+        (\time_r keepuses -> M.insert id time_r $! M.intersection maps $ M.fromList keepuses)
+        mnub
+        gettime
+        M.filter
+        uses' incr' timecheckmodulo maps minimumkeep removalsize
+    
+
+{-
+-- This function differs from 'lookupECM' only in the case that the value
+-- being requested also causes a new time to have been computed during the 
+-- same lookup, and have been found to be out of date. When the condition 
+-- happens, this function returns the old cached value without attempting
+-- to request a new value, despite being out of date. However, it does
+-- clear the key from the key-value store for the next request.
+--
+lookupECMUse :: (Monad m, Ord k) => ECM m mv s M.Map k v -> k -> m v
+lookupECMUse ecm id = do
+  enter m'maps $
+    \(CacheState (retr_state, maps, uses, incr)) ->
+      let incr' = incr + 1
+       in if incr' < incr
+            -- Word incrementor has cycled back to 0,
+            -- so may as well clear the cache completely.
+            then lookupECM' (retr_state, M.empty, ([], 0), 0) (0+1)
+            else lookupECM' (retr_state, maps, uses, incr) incr'
+  where
+    
+    ECM ( m'maps, retr, gettime, minimumkeep, timecheckmodulo, removalsize,
+          compactlistsize, enter, _ro ) = ecm
+  
+    mnub = M.toList . M.fromList . reverse
+    lookupECM' (retr_state, maps, uses, incr) incr' = do
+      let uses' = updateUses uses id incr' compactlistsize mnub
+      (ret, _) <-
+          detECM (M.lookup id maps) retr_state (retr retr_state id)
+            (\time_r -> M.insert id time_r maps)
+            (\time_r keepuses -> M.insert id time_r $! M.intersection maps $ M.fromList keepuses)
+            mnub
+            gettime
+            M.filter
+            uses' incr' timecheckmodulo maps minimumkeep removalsize
+      return ret
+-}
+
+
+-- | Used with 'newECMIO' or 'newECMForM' to provide a consistent duration for requested values.
+consistentDuration :: (Monad m, Ord k) => TimeUnits -> (Maybe s -> k -> m (Maybe s, v)) -> (Maybe s -> k -> m (TimeUnits, (Maybe s, v)))
+consistentDuration duration fun =
+  \state id -> do
+    ret <- fun state id
+    return (duration, ret)
+
+ Caching/ExpiringCacheMap/Types.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module : Caching.ExpiringCacheMap.Types
+-- Copyright: (c) 2014 Edward L. Blake
+-- License: BSD-style
+-- Maintainer: Edward L. Blake <edwardlblake@gmail.com>
+-- Stability: experimental
+-- Portability: portable
+--
+-- Types common to "Caching.ExpiringCacheMap.OrdECM" and "Caching.ExpiringCacheMap.HashECM".
+-- 
+
+module Caching.ExpiringCacheMap.Types (
+    -- * Configuration
+    CacheSettings(..),
+    -- * Cache encapsulation
+    ECM,
+    CacheState,
+    -- * Types
+    TimeUnits,
+    ECMMapSize,
+    ECMULength,
+    ECMIncr,
+    -- * Types for state function
+    ECMNewState,
+    ECMEnterState,
+    ECMReadState,
+) where
+
+import Caching.ExpiringCacheMap.Utils.Types
+import Caching.ExpiringCacheMap.Internal.Types
+
+data CacheSettings =
+  CacheWithLRUList {
+    mapsize :: ECMMapSize,
+    removalsize :: ECMULength,
+    compactlistsize :: ECMULength
+  }
+ Caching/ExpiringCacheMap/Utils/TestSequence.hs view
@@ -0,0 +1,163 @@+-- |
+-- Module : Caching.ExpiringCacheMap.Utils.TestSequence
+-- Copyright: (c) 2014 Edward L. Blake
+-- License: BSD-style
+-- Maintainer: Edward L. Blake <edwardlblake@gmail.com>
+-- Stability: experimental
+-- Portability: portable
+--
+-- TestSequence monad for testing caching behaviour.
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- > 
+-- > module TestSequenceExample where
+-- > 
+-- > import Caching.ExpiringCacheMap.HashECM (newECMForM, lookupECM, CacheSettings(..), consistentDuration)
+-- > import qualified Caching.ExpiringCacheMap.Utils.TestSequence as TestSeq
+-- > 
+-- > import qualified Data.ByteString.Char8 as BS
+-- > 
+-- > test = do
+-- >   (TestSeq.TestSequenceState (_, events, _), return_value) <- TestSeq.runTestSequence test'
+-- >   (putStrLn . show . reverse) events
+-- >   return ()
+-- >   where
+-- >     test' = do
+-- >       filecache <- newECMForM
+-- >             (consistentDuration 100 -- Duration between access and expiry time of each item, no state needed.
+-- >               (\state _id -> do number <- TestSeq.readNumber
+-- >                                 return (state, number)))
+-- >             (TestSeq.getCurrentTime >>= return)
+-- >             12000 -- Time check frequency: (accumulator `mod` this_number) == 0.
+-- >             (CacheWithLRUList 
+-- >               6   -- Expected size of key-value map when removing elements.
+-- >               6   -- Size of list when to remove items from key-value map.
+-- >               12  -- Size of list when to compact
+-- >               )
+-- >             TestSeq.newTestSVar TestSeq.enterTestSVar TestSeq.readTestSVar
+-- >       
+-- >       -- Use lookupECM whenever the contents of "file1" is needed.
+-- >       b <- lookupECM filecache ("file1" :: BS.ByteString)
+-- >       TestSeq.haveNumber b
+-- >       b <- lookupECM filecache "file1"
+-- >       b <- lookupECM filecache "file2"
+-- >       TestSeq.haveNumber b
+-- >       return b
+-- >
+--
+-- >>> test
+-- [GetVar 3,ReadNumber 4,GetTime 7,PutVar 11,HaveNumber 4,GetVar 14,PutVar 17,
+--  GetVar 19,ReadNumber 20,GetTime 23,PutVar 27,HaveNumber 20]
+-- 
+--
+
+module Caching.ExpiringCacheMap.Utils.TestSequence (
+    runTestSequence,
+    newTestSVar,
+    enterTestSVar,
+    readTestSVar,
+    getCurrentTime,
+    readNumber,
+    haveNumber,
+    TestSequenceEvents(..),
+    TestSequenceState(..),
+    TestSequence(..),
+    TestSVar(..)
+) where
+
+import Data.Word (Word32)
+
+data TestSequenceEvents = 
+  GetVar Word32 |
+  PutVar Word32 |
+  GetTime Word32 |
+  ReadNumber Int |
+  HaveNumber Int
+  deriving (Eq)
+
+instance Show TestSequenceEvents where
+  show (GetVar a)     = "GetVar " ++ (show a)
+  show (PutVar a)     = "PutVar " ++ (show a)
+  show (GetTime a)    = "GetTime " ++ (show a)
+  show (ReadNumber a) = "ReadNumber " ++ (show a)
+  show (HaveNumber a) = "HaveNumber " ++ (show a)
+
+
+newtype TestSequenceState b =
+  TestSequenceState (Word32, [TestSequenceEvents], Maybe b)
+  
+instance Show (TestSequenceState ct) where
+  show (TestSequenceState (a,b,_)) =
+    "TestSequenceState " ++ (show a) ++ " " ++ (show b)
+
+newtype TestSequence b a =
+  TestSequence (TestSequenceState b -> (TestSequenceState b, a))
+
+newtype TestSVar a = TestSVar a
+
+
+instance Monad (TestSequence a) where
+  TestSequence fun >>= k =
+    TestSequence
+      (\state -> let (state', ret) = (fun state)
+                     TestSequence fun' = k ret
+                  in fun' state')
+  return ret = 
+    TestSequence $
+      \(TestSequenceState (timer, hl, testsvar)) ->
+       (TestSequenceState (timer+1,hl, testsvar), ret)
+
+runTestSequence :: Show a => TestSequence b a -> IO (TestSequenceState b, a)
+runTestSequence f = do
+  let ret = (fun (TestSequenceState (0, [], Nothing)))
+   in return ret
+  where
+    TestSequence fun = (TestSequence
+      (\(TestSequenceState (t, hl, testsvar)) ->
+        (TestSequenceState (t+1, hl, testsvar), ()))) >> f
+
+newTestSVar :: a -> TestSequence a (TestSVar a)
+newTestSVar var = TestSequence $
+  \(TestSequenceState (timer, hl, Nothing)) ->
+   (TestSequenceState (timer+1, hl, Just var), TestSVar var)
+
+enterTestSVar :: TestSVar a -> (a -> TestSequence a (a,b)) -> TestSequence a b
+enterTestSVar testsvar fun = do
+  teststate <- readTestSVar testsvar
+  (teststate',passalong) <- fun teststate
+  putTestSVar testsvar teststate'
+  return passalong
+
+-- 'putTestSVar' is used along with 'readTestSVar' to implement enterTestSVar.
+--
+putTestSVar :: TestSVar a -> a -> TestSequence a a
+putTestSVar _testsvar testsvar' = TestSequence $
+  \(TestSequenceState (timer, hl, testsvar)) ->
+   (TestSequenceState (timer+1, (PutVar timer) : hl, Just testsvar'),
+      case testsvar of
+        Nothing -> testsvar'
+        Just testsvar'' -> testsvar'')
+
+readTestSVar :: TestSVar a -> TestSequence a a
+readTestSVar _testsvar = TestSequence $
+  \(TestSequenceState (timer, hl, Just testsvar)) ->
+   (TestSequenceState (timer+1, (GetVar timer) : hl, Just testsvar), testsvar)
+
+getCurrentTime :: TestSequence a Int
+getCurrentTime = TestSequence $
+  \(TestSequenceState (timer, hl, testsvar)) ->
+   (TestSequenceState (timer+1, (GetTime timer) : hl, testsvar), fromIntegral timer)
+
+readNumber :: TestSequence a Int
+readNumber = TestSequence $
+  \(TestSequenceState (timer, hl, testsvar)) ->
+    let number = fromIntegral timer
+     in (TestSequenceState (timer+1, (ReadNumber number) : hl, testsvar), number)
+
+haveNumber :: Int -> TestSequence a ()
+haveNumber number = TestSequence $
+  \(TestSequenceState (timer, hl, testsvar)) ->
+   (TestSequenceState (timer+1, (HaveNumber number) : hl, testsvar), ())
+
+
+
+ Caching/ExpiringCacheMap/Utils/Types.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module : Caching.ExpiringCacheMap.Utils.Types
+-- Copyright: (c) 2014 Edward L. Blake
+-- License: BSD-style
+-- Maintainer: Edward L. Blake <edwardlblake@gmail.com>
+-- Stability: experimental
+-- Portability: portable
+--
+-- Simple types.
+-- 
+
+module Caching.ExpiringCacheMap.Utils.Types (
+    -- * Types
+    TimeUnits,
+    ECMMapSize,
+    ECMULength,
+    ECMIncr,
+) where
+
+import Data.Word (Word32)
+
+-- | Integer involved in the time units used to determine when an item expires.
+-- The time units used can be any arbitrary integer time representation, such
+-- as seconds or milliseconds for examples. They can also be deterministic time
+-- steps in a sequencing monad.
+--
+type TimeUnits = Int
+
+-- | Integer involved in the size of a key-value map.
+type ECMMapSize = Int
+
+-- | Integer involved in the length of the usage history list.
+type ECMULength = Int
+
+-- | Unsigned integer ('Word32') involved in the cache state incrementing accumulator.
+type ECMIncr = Word32
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Edward L. Blake
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Edward L. Blake nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,7 @@+
+expiring-cache-map
+==================
+
+A general purpose simple shared state cache map with automatic expiration of
+values for caching the results of accessing a resource (such as reading a file),
+with variations for Ord and Hashable keys.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple
+main = defaultMain
+ expiring-cache-map.cabal view
@@ -0,0 +1,68 @@+name:                expiring-cache-map
+version:             0.0.5.0
+synopsis:            General purpose simple caching.
+description:         
+    A simple general purpose shared state cache map with automatic expiration 
+    of values, for caching the results of accessing a resource such as reading 
+    a file. With variations for Ord and Hashable keys using "Data.Map.Strict"
+    and "Data.HashMap.Strict", respectively.
+
+homepage:            https://github.com/elblake/expiring-cache-map
+bug-reports:         https://github.com/elblake/expiring-cache-map/issues
+license:             BSD3
+license-file:        LICENSE
+author:              Edward L. Blake
+maintainer:          edwardlblake@gmail.com
+copyright:           (c) 2014 Edward L. Blake
+category:            Caching
+build-type:          Simple
+cabal-version:       >=1.8
+
+extra-source-files:
+    README.md
+
+library
+  exposed-modules:
+    Caching.ExpiringCacheMap.OrdECM
+    Caching.ExpiringCacheMap.HashECM
+    Caching.ExpiringCacheMap.Internal.Internal
+    Caching.ExpiringCacheMap.Internal.Types
+    Caching.ExpiringCacheMap.Types
+    Caching.ExpiringCacheMap.Utils.TestSequence
+    Caching.ExpiringCacheMap.Utils.Types
+  -- other-modules:       
+  build-depends:       
+    base == 4.*,
+    containers >= 0.5.0.0,
+    hashable >= 1.0.1.1 && < 1.2,
+    unordered-containers >= 0.2.0.0
+
+test-suite test-threads
+    type: exitcode-stdio-1.0
+    hs-source-dirs: . tests
+    main-is: TestWithThreads.hs
+    build-depends:
+      base,
+      bytestring >= 0.10.0.0,
+      time >= 1.0,
+      containers >= 0.5.0.0,
+      hashable >= 1.0.1.1 && < 1.2,
+      unordered-containers >= 0.2.0.0
+    other-modules:
+      TestHashECMWithThreads
+      TestOrdECMWithThreads
+      Caching.ExpiringCacheMap.Internal.Internal
+
+test-suite test-sequence
+    type: exitcode-stdio-1.0
+    hs-source-dirs: . tests
+    main-is: TestWithTestSequence.hs
+    build-depends:
+      base,
+      bytestring >= 0.10.0.0,
+      containers >= 0.5.0.0,
+      hashable >= 1.0.1.1 && < 1.2,
+      unordered-containers >= 0.2.0.0
+    other-modules:
+      TestHashECMWithTestSequence
+      TestOrdECMWithTestSequence
+ tests/TestHashECMWithTestSequence.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}
+
+module TestHashECMWithTestSequence where
+
+import Caching.ExpiringCacheMap.HashECM (newECMForM, lookupECM, CacheSettings(..), consistentDuration)
+import qualified Caching.ExpiringCacheMap.Utils.TestSequence as TestSeq
+
+import qualified Data.ByteString.Char8 as BS
+
+numberEventsOnly a =
+  case a of
+    TestSeq.ReadNumber _ -> True
+    TestSeq.HaveNumber _ -> True
+    _ -> False
+
+testWithTestSequence = do
+  (TestSeq.TestSequenceState (_, events, _), return_value) <- TestSeq.runTestSequence test'
+  case (filter numberEventsOnly . reverse) events of
+    [ TestSeq.ReadNumber numr1, TestSeq.HaveNumber numh1,
+      TestSeq.ReadNumber numr2, TestSeq.HaveNumber numh2 ]
+      | numr1 == numh1 && numr2 == numh2 -> do
+        (putStrLn . show . filter numberEventsOnly . reverse) events
+        return ()
+  where
+    test' = do
+      filecache <- newECMForM
+            (consistentDuration 100 -- Duration between access and expiry time of each item, no state needed.
+              (\state _id -> do number <- TestSeq.readNumber
+                                return (state, number)))
+            (TestSeq.getCurrentTime >>= return)
+            12000 -- Time check frequency: (accumulator `mod` this_number) == 0.
+            (CacheWithLRUList 
+              6   -- Expected size of key-value map when removing elements.
+              6   -- Size of list when to remove items from key-value map.
+              12  -- Size of list when to compact
+              )
+            TestSeq.newTestSVar TestSeq.enterTestSVar TestSeq.readTestSVar
+      
+      -- Use lookupECM whenever the contents of "file1" is needed.
+      b <- lookupECM filecache ("file1" :: BS.ByteString)
+      TestSeq.haveNumber b
+      b <- lookupECM filecache "file1"
+      b <- lookupECM filecache "file2"
+      TestSeq.haveNumber b
+      return b
+
+main = testWithTestSequence
+
+ 
+ tests/TestHashECMWithThreads.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE OverloadedStrings #-}
+
+--
+-- Test HashECM with threads
+--
+
+module TestHashECMWithThreads where
+
+import Control.Concurrent (forkIO, threadDelay)
+import qualified Data.Time.Clock.POSIX as POSIX (POSIXTime, getPOSIXTime)
+import qualified Data.ByteString.Lazy.Char8 as LBS
+
+import qualified Control.Concurrent.MVar as MV
+import qualified Data.HashMap.Strict as HM
+import Data.Hashable (Hashable(..))
+
+import Caching.ExpiringCacheMap.HashECM
+import Caching.ExpiringCacheMap.Internal.Internal (getStatsString)
+
+testWithThreads = do
+  ecm <- newECMIO
+            (consistentDuration 10
+              (\state id -> do LBS.putStrLn id; return (state, [])))
+            (do time <- POSIX.getPOSIXTime
+                return (round (time * 100)))
+            120 
+            (CacheWithLRUList 6 6 12) :: IO (ECM IO MV.MVar () HM.HashMap LBS.ByteString [Int])
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.2"
+      threadDelay 2
+      return ())
+      [0..400]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.5"
+      threadDelay 5
+      return ())
+      [0..300]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.7"
+      threadDelay 7
+      return ())
+      [0..300]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.11"
+      threadDelay 11
+      return ())
+      [0..200]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.13"
+      threadDelay 13
+      return ())
+      [0..200]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.17"
+      threadDelay 17
+      return ())
+      [0..200]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.111"
+      threadDelay 111
+      return ())
+      [0..20]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.109"
+      threadDelay 109
+      return ())
+      [0..20]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.1091"
+      threadDelay 1091
+      return ())
+      [0..5]
+  threadDelay 2000000
+  c <- getStatsString ecm
+  putStrLn c
+  return ()
+ tests/TestOrdECMWithTestSequence.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}
+
+module TestOrdECMWithTestSequence where
+
+import Caching.ExpiringCacheMap.OrdECM (newECMForM, lookupECM, CacheSettings(..), consistentDuration)
+import qualified Caching.ExpiringCacheMap.Utils.TestSequence as TestSeq
+
+import qualified Data.ByteString.Char8 as BS
+
+numberEventsOnly a =
+  case a of
+    TestSeq.ReadNumber _ -> True
+    TestSeq.HaveNumber _ -> True
+    _ -> False
+
+testWithTestSequence = do
+  (TestSeq.TestSequenceState (_, events, _), return_value) <- TestSeq.runTestSequence test'
+  case (filter numberEventsOnly . reverse) events of
+    [ TestSeq.ReadNumber numr1, TestSeq.HaveNumber numh1,
+      TestSeq.ReadNumber numr2, TestSeq.HaveNumber numh2 ]
+      | numr1 == numh1 && numr2 == numh2 -> do
+        (putStrLn . show . filter numberEventsOnly . reverse) events
+        return ()
+  where
+    test' = do
+      filecache <- newECMForM
+            (consistentDuration 100 -- Duration between access and expiry time of each item, no state needed.
+              (\state _id -> do number <- TestSeq.readNumber
+                                return (state, number)))
+            (TestSeq.getCurrentTime >>= return)
+            12000 -- Time check frequency: (accumulator `mod` this_number) == 0.
+            (CacheWithLRUList 
+              6   -- Expected size of key-value map when removing elements.
+              6   -- Size of list when to remove items from key-value map.
+              12  -- Size of list when to compact
+              )
+            TestSeq.newTestSVar TestSeq.enterTestSVar TestSeq.readTestSVar
+      
+      -- Use lookupECM whenever the contents of "file1" is needed.
+      b <- lookupECM filecache ("file1" :: BS.ByteString)
+      TestSeq.haveNumber b
+      b <- lookupECM filecache "file1"
+      b <- lookupECM filecache "file2"
+      TestSeq.haveNumber b
+      return b
+
+main = testWithTestSequence
+
+ 
+ tests/TestOrdECMWithThreads.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE OverloadedStrings #-}
+
+--
+-- Test OrdECM with threads
+--
+
+module TestOrdECMWithThreads where
+
+import Control.Concurrent (forkIO, threadDelay)
+import qualified Data.Time.Clock.POSIX as POSIX (POSIXTime, getPOSIXTime)
+import qualified Data.ByteString.Lazy.Char8 as LBS
+
+import qualified Control.Concurrent.MVar as MV
+import qualified Data.Map as M
+
+import Caching.ExpiringCacheMap.OrdECM
+import Caching.ExpiringCacheMap.Internal.Internal (getStatsString)
+
+testWithThreads = do
+  ecm <- newECMIO
+            (consistentDuration 10
+              (\state id -> do LBS.putStrLn id; return (state, [])))
+            (do time <- POSIX.getPOSIXTime
+                return (round (time * 100)))
+            120
+            (CacheWithLRUList 6 6 12 ) :: IO (ECM IO MV.MVar () M.Map LBS.ByteString [Int])
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.2"
+      threadDelay 2
+      return ())
+      [0..400]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.5"
+      threadDelay 5
+      return ())
+      [0..300]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.7"
+      threadDelay 7
+      return ())
+      [0..300]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.11"
+      threadDelay 11
+      return ())
+      [0..200]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.13"
+      threadDelay 13
+      return ())
+      [0..200]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.17"
+      threadDelay 17
+      return ())
+      [0..200]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.111"
+      threadDelay 111
+      return ())
+      [0..20]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.109"
+      threadDelay 109
+      return ())
+      [0..20]
+  forkIO $ do
+    mapM_ (\a -> do
+      b <- lookupECM ecm "test.1091"
+      threadDelay 1091
+      return ())
+      [0..5]
+  threadDelay 2000000
+  c <- getStatsString ecm
+  putStrLn c
+  return ()
+ tests/TestWithTestSequence.hs view
@@ -0,0 +1,9 @@+-- module TestWithTestSequence where
+
+import qualified TestOrdECMWithTestSequence as OrdTest
+import qualified TestHashECMWithTestSequence as HashTest
+
+main = do
+  HashTest.testWithTestSequence
+  OrdTest.testWithTestSequence
+  return ()
+ tests/TestWithThreads.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE OverloadedStrings #-}
+
+--
+-- Test with threads
+--
+
+-- module TestWithThreads where
+
+import qualified TestOrdECMWithThreads as OrdTest
+import qualified TestHashECMWithThreads as HashTest
+
+testWithThreads = do
+  HashTest.testWithThreads
+  OrdTest.testWithThreads
+  return ()
+
+main = testWithThreads