packages feed

covenant-1.0.0: src/Control/Monad/HashCons.hs

{-# LANGUAGE FunctionalDependencies #-}

-- | Module: Control.Monad.HashCons
--
-- Provides a transformer, and a capability type class in the style of @mtl@,
-- for hash consing. See the Covenant wiki for how this works.
module Control.Monad.HashCons
  ( -- * Transformer
    HashConsT,
    runHashConsT,
    hashCons,
    lookupRef_,

    -- * Capability type class
    MonadHashCons (..),
  )
where

import Control.Monad.State.Strict
  ( StateT,
    get,
    modify,
    runStateT,
  )
import Control.Monad.Trans (MonadTrans (lift))
import Control.Monad.Trans.Except (ExceptT)
import Control.Monad.Trans.Maybe (MaybeT)
import Control.Monad.Trans.RWS.CPS (RWST)
import Control.Monad.Trans.Reader (ReaderT)
import Control.Monad.Trans.Writer.CPS (WriterT)
import Data.Bimap (Bimap)
import Data.Bimap qualified as Bimap
import Data.Kind (Type)

-- | A transformer implementing hash consing capabilities, with references of
-- type @r@ and referents of type @e@. It is assumed that values of type @e@
-- contain values of type @r@ in their capacity as references, though this is
-- not a requirement of this transformer.
--
-- = Important note
--
-- This implementation is not suitable for any @m@ that throws exceptions. This
-- includes @IO@, @ST@ and anything stacked atop them. For the reasons why, see
-- [here](https://github.com/haskell-effectful/effectful/blob/master/transformers.md#statet).
--
-- @since 1.0.0
newtype HashConsT (r :: Type) (e :: Type) (m :: Type -> Type) (a :: Type)
  = HashConsT (StateT (Bimap r e) m a)
  deriving
    ( -- | @since 1.0.0
      Functor,
      -- | @since 1.0.0
      Applicative,
      -- | @since 1.0.0
      Monad
    )
    via (StateT (Bimap r e) m)
  deriving
    ( -- | @since 1.0.0
      MonadTrans
    )
    via StateT (Bimap r e)

-- | Execute the computation described, returning both the result and the unique
-- pairings of @r@ and @e@ produced as part of it.
--
-- @since 1.0.0
runHashConsT ::
  forall (r :: Type) (e :: Type) (m :: Type -> Type) (a :: Type).
  HashConsT r e m a ->
  m (a, Bimap r e)
runHashConsT (HashConsT comp) = runStateT comp Bimap.empty

-- | Given a value of type @e@, produce the unique value of type @r@ acting as a
-- reference to it. This @r@ will be cached, ensuring any future requests for
-- the reference for this value of type @e@ will be the same.
--
-- @since 1.0.0
hashCons ::
  forall (r :: Type) (e :: Type) (m :: Type -> Type).
  (Ord r, Ord e, Bounded r, Enum r, Monad m) =>
  e ->
  HashConsT r e m r
hashCons x = HashConsT $ do
  binds <- get
  case Bimap.lookupR x binds of
    Nothing ->
      if Bimap.null binds
        then minBound <$ modify (Bimap.insert minBound x)
        else do
          let largestOldRef = fst . Bimap.findMax $ binds
          let newRef = succ largestOldRef
          newRef <$ modify (Bimap.insert newRef x)
    Just ref -> pure ref

-- | Given a value of type @r@, fetch the cached @e@ value, if it exists.
--
-- @since 1.0.0
lookupRef_ ::
  forall (r :: Type) (e :: Type) (m :: Type -> Type).
  (Monad m, Ord e, Ord r) =>
  r ->
  HashConsT r e m (Maybe e)
lookupRef_ r = HashConsT (Bimap.lookup r <$> get)

-- | An @mtl@-style capability type class for hash consing capability, using
-- references of type @r@ and values of type @e@.
--
-- = Laws
--
-- 1. @'refTo' x '>>' 'refTo' x@ @=@ @'refTo' x@
-- 2. @'liftA2' ('/=') ('refTo' x) ('refTo' y)@ @=@ @'refTo' x '*>' 'refTo' y '*>' 'pure' (x '/=' y)@
-- 3. @'refTo' x '>>=' (\\r -> 'lookupRef' r '>>=' (\\y -> 'pure' (y, r)))@ @=@ @('Just' x, ) '<$>' 'refTo' x@
--
-- @since 1.0.0
class
  (Eq e, Eq r, Monad m) =>
  MonadHashCons (r :: Type) (e :: Type) (m :: Type -> Type)
    | m -> e r
  where
  -- | Produce the unique value of type @r@ that acts as a reference for the
  -- given value of type @e@.
  --
  -- @since 1.0.0
  refTo :: e -> m r

  -- | Given a value of type @r@, fetch the cached value of type @e@.
  --
  -- @since 1.0.0
  lookupRef :: r -> m (Maybe e)

-- | @since 1.0.0
instance (Ord r, Ord e, Bounded r, Enum r, Monad m) => MonadHashCons r e (HashConsT r e m) where
  {-# INLINEABLE refTo #-}
  refTo = hashCons
  {-# INLINEABLE lookupRef #-}
  lookupRef = lookupRef_

-- | @since 1.0.0
instance (Ord r, Ord e, MonadHashCons r e m) => MonadHashCons r e (MaybeT m) where
  {-# INLINEABLE refTo #-}
  refTo e = lift (refTo e)
  {-# INLINEABLE lookupRef #-}
  lookupRef r = lift (lookupRef r)

-- | @since 1.0.0
instance (MonadHashCons r e m) => MonadHashCons r e (ReaderT r' m) where
  {-# INLINEABLE refTo #-}
  refTo e = lift (refTo e)
  {-# INLINEABLE lookupRef #-}
  lookupRef r = lift (lookupRef r)

-- | @since 1.0.0
instance (MonadHashCons r e m) => MonadHashCons r e (StateT s m) where
  {-# INLINEABLE refTo #-}
  refTo e = lift (refTo e)
  {-# INLINEABLE lookupRef #-}
  lookupRef r = lift (lookupRef r)

-- | @since 1.0.0
instance (MonadHashCons r e m) => MonadHashCons r e (WriterT w m) where
  {-# INLINEABLE refTo #-}
  refTo e = lift (refTo e)
  {-# INLINEABLE lookupRef #-}
  lookupRef r = lift (lookupRef r)

-- | @since 1.0.0
instance (MonadHashCons r e m) => MonadHashCons r e (RWST r' w s m) where
  {-# INLINEABLE refTo #-}
  refTo e = lift (refTo e)
  {-# INLINEABLE lookupRef #-}
  lookupRef r = lift (lookupRef r)

-- | @since 1.0.0
instance (MonadHashCons r e m) => MonadHashCons r e (ExceptT e' m) where
  {-# INLINEABLE refTo #-}
  refTo e = lift (refTo e)
  {-# INLINEABLE lookupRef #-}
  lookupRef r = lift (lookupRef r)