packages feed

microecta-0.1.0.0: src/Data/Interned/Extended/HashTableBased.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE TypeFamilies #-}

-- | Tiny hash-consing abstraction backed by mutable cuckoo hash tables.
module Data.Interned.Extended.HashTableBased (
    Id,
    Cache (..),
    freshCache,
    Interned (..),
    intern,
) where

import qualified Data.HashTable.IO as HT
import Data.Hashable
import Data.IORef
import GHC.IO (unsafeDupablePerformIO)

-- | Dense identity assigned to each interned value.
type Id = Int

{- | Tried using the BasicHashtable size function to remove need for this IORef
(see https://github.com/gregorycollins/hashtables/pull/68), but it was slower.
-}
data Cache t = Cache
    { fresh :: !(IORef Id)
    -- ^ Next id to allocate.
    , content :: !(HT.CuckooHashTable (Description t) t)
    -- ^ Map from structural descriptions to canonical interned values.
    }

-- | Allocate an empty interning cache.
freshCache :: IO (Cache t)
freshCache =
    Cache
        <$> newIORef 0
        <*> HT.new

-- | Values that can be hash-consed through a global cache.
class
    ( Eq (Description t)
    , Hashable (Description t)
    ) =>
    Interned t
    where
    -- | Hashable structural representation used as the cache key.
    data Description t

    -- | Non-canonical input used to build an interned value.
    type Uninterned t

    -- | Compute the cache key for an uninterned value.
    describe :: Uninterned t -> Description t

    -- | Attach a freshly allocated identity to an uninterned value.
    identify :: Id -> Uninterned t -> t

    -- | Process-global cache for this interned type.
    cache :: Cache t

-- | Return the canonical interned representative for an uninterned value.
intern :: (Interned t) => Uninterned t -> t
intern !bt = unsafeDupablePerformIO $ do
    let c = cache
    let refI = fresh c
    let ht = content c
    v <- HT.lookup ht dt
    case v of
        Nothing -> do
            i <- atomicModifyIORef' refI (\i -> (i + 1, i))
            let t = identify i bt
            HT.insert ht dt t
            return t
        Just t -> return t
  where
    !dt = describe bt