packages feed

morley-1.20.0: src/Morley/Util/MultiReader.hs

-- SPDX-FileCopyrightText: 2022 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- | A poor man's extensible reader effects via nested 'ReaderT'.
module Morley.Util.MultiReader
  ( MultiReaderT
  , MonadMultiReaderT
  , ChangeMultiReaderBase
  , asks'
  , ask'
  , local'
  , mapMultiReaderT
  ) where

import Control.Monad.Reader (mapReaderT)
import GHC.TypeLits (ErrorMessage(..), TypeError)

import Morley.Util.Peano

-- | Convenience type family to build a stack of multiple 'ReaderT'.
type family MultiReaderT (xs :: [Type]) (m :: Type -> Type) :: Type -> Type where
  MultiReaderT (x ': xs) m = ReaderT x (MultiReaderT xs m)
  MultiReaderT '[] m = m

-- | Convenience constraint synonym.
--
-- Required for `asks'`, `ask'`, `local'` and 'mapMultiReaderT'
type MonadMultiReaderT m base =
  (m ~ MultiReaderT (MultiReaderIso m) base, MonadMultiReaderMap (MultiReaderIso m))

-- | Replace the base monad for a nested 'ReaderT' stack.
type ChangeMultiReaderBase m newBase = MultiReaderT (MultiReaderIso m) newBase

-- | Find the index of the first occurrence of the first argument in the second
-- argument as a Peano number.
--
-- Essentially a type-level version of 'find'.
--
-- Raises a type error if the element is not found.
type family MultiReaderDepth (r :: Type) (rs :: [Type]) :: Peano where
  MultiReaderDepth r (r ': _) = 'Z
  MultiReaderDepth r (_ ': rs) = 'S (MultiReaderDepth r rs)
  MultiReaderDepth r '[] = TypeError (
    'Text "MultiReaderT does not have a reader environment" ':$$:
    'ShowType r ':$$: 'Text "anywhere in the stack."
    )

-- | Given a transformer stack of nested 'ReaderT', get a list of environments.
-- This type family is essentially a witness of isomorphism between a stack of
-- 'ReaderT' and a type-level list of reader environments.
--
-- This is useful because 'MultiReaderT' can't have an injectivity annotation.
type family MultiReaderIso (m :: Type -> Type) :: [Type] where
  MultiReaderIso (ReaderT r m) = r ': MultiReaderIso m
  MultiReaderIso _ = '[]

-- | Typeclass implementing versions of 'ask' and 'local' that aren't
-- constrained by a functional dependency.
class (Monad m, n ~ MultiReaderDepth r (MultiReaderIso m))
  => MultiReader (n :: Peano) r m where
  -- | Unconstrained version of 'ask'. Lifts the appropriate number of times
  -- depending on the type @r@.
  ask' :: m r

  -- | Unconstrained version of 'local'. Maps the appropriate number of times
  -- depending on the type @r@.
  local' :: (r -> r) -> m a -> m a

instance (Monad m) => MultiReader 'Z x (ReaderT x m) where
  ask' = ask
  local' = local

instance
  ( MultiReader n r m, 'S n ~ MultiReaderDepth r (x ': MultiReaderIso m) )
  => MultiReader ('S n) r (ReaderT x m) where
  ask' = lift ask'
  local' = mapReaderT . local'

-- | Class implementing 'mapReaderT' over a stack of 'ReaderT'.
class MonadMultiReaderMap xs where
  -- | 'mapReaderT', only it maps over the whole nested 'ReaderT' stack, and not
  -- just one level.
  mapMultiReaderT
    :: ( m' ~ MultiReaderT xs m
       , n' ~ MultiReaderT xs n
       , xs ~ MultiReaderIso m'
       )
    => (m a -> n b) -> m' a -> n' b

instance MonadMultiReaderMap '[] where
  mapMultiReaderT f = f

instance (MonadMultiReaderMap xs) => MonadMultiReaderMap (x ': xs) where
  mapMultiReaderT f = mapReaderT (mapMultiReaderT f)

-- | Unconstrained version of 'asks'. @asks' f = fmap f ask'@.
asks' :: forall m r (a :: Type) n. MultiReader n r m => (r -> a) -> m a
asks' f = f <$> ask'