packages feed

microecta-0.1.0.0: src/Data/Memoization.hs

{-# LANGUAGE OverloadedStrings #-}

{- | Quick-and-dirty, thread-unsafe, hash-based memoization.

The ECTA core relies on stable global memo tables for interning and recursive
graph operations. This module intentionally keeps that machinery tiny: each
call to 'memo' allocates one process-global hash table through
'unsafePerformIO'.
-}
module Data.Memoization (
    MemoCacheTag (..),
    memo,
    memo2,
) where

import qualified Data.HashTable.IO as HT
import Data.Hashable (Hashable (..))
import Data.Text (Text)
import System.IO.Unsafe (unsafePerformIO)

-- | Human-readable name for a memo table.
data MemoCacheTag
    = -- | Name a table for debugging and nested-table derivation.
      NameTag Text
    deriving (Eq, Ord, Show)

instance Hashable MemoCacheTag where
    hashWithSalt salt (NameTag name) = salt `hashWithSalt` name

mkInnerTag :: MemoCacheTag -> MemoCacheTag
mkInnerTag (NameTag t) = NameTag (t <> "-inner")

memoIO :: forall a b. (Eq a, Hashable a) => MemoCacheTag -> (a -> b) -> IO (a -> IO b)
memoIO _ f = do
    ht :: HT.CuckooHashTable a b <- HT.new
    let f' x = do
            v <- HT.lookup ht x
            case v of
                Nothing -> do
                    let r = f x
                    HT.insert ht x r
                    return r
                Just r -> return r
    return f'

-- | Memoize a pure unary function in a process-global mutable hash table.
memo :: (Eq a, Hashable a) => MemoCacheTag -> (a -> b) -> (a -> b)
memo tag f =
    let f' = unsafePerformIO (memoIO tag f)
     in \x -> unsafePerformIO (f' x)

-- | Memoize a pure binary function as nested unary memo tables.
memo2 :: (Eq a, Hashable a, Eq b, Hashable b) => MemoCacheTag -> (a -> b -> c) -> a -> b -> c
memo2 tag f = memo tag (memo (mkInnerTag tag) . f)