Add insertAt

A combinator that allows adding effects at a specified index into the
effect stack
This commit is contained in:
Torsten Schmits 2021-04-20 23:33:42 +02:00 committed by Torsten Schmits
parent e25d1207d1
commit 6e2630fafd
13 changed files with 276 additions and 52 deletions

View File

@ -4,6 +4,7 @@
* Deprecate `traceToIO` and replace it with `traceToStdout` and `traceToStderr` * Deprecate `traceToIO` and replace it with `traceToStdout` and `traceToStderr`
* Support GHC 9.0.1 * Support GHC 9.0.1
* Added the combinator `insertAt`, which allows adding effects at a specified index into the effect stack
## 1.5.0.0 (2021-03-30) ## 1.5.0.0 (2021-03-30)

View File

@ -80,6 +80,7 @@ test-suite polysemy-plugin-test
BadSpec BadSpec
DoctestSpec DoctestSpec
ExampleSpec ExampleSpec
InsertSpec
LegitimateTypeErrorSpec LegitimateTypeErrorSpec
MultipleVarsSpec MultipleVarsSpec
PluginSpec PluginSpec

View File

@ -0,0 +1,22 @@
module InsertSpec where
import Polysemy.State (State)
import Polysemy.Reader (Reader)
import Polysemy.Internal (insertAt)
import Polysemy
import Test.Hspec
insertAtWithIndex :: Sem (e1 : e2 : e3 : e4 : r) a -> Sem (e1 : e2 : Reader i : e3 : e4 : r) a
insertAtWithIndex = insertAt @2
insertAtAndRaiseUnder :: Sem (e1 : r) a -> Sem (e1 : e2 : State s : e3 : Reader i : e4 : r) a
insertAtAndRaiseUnder = raise2Under . insertAt @2 . raiseUnder
insertAtEmpty :: Sem (e1 : e2 : r) a -> Sem (e1 : e2 : r) a
insertAtEmpty = insertAt @2
spec :: Spec
spec = parallel $ do
describe "insert" $ do
it "insert" $ do
1 `shouldBe` 1

View File

@ -9,6 +9,7 @@ module TypeErrors where
-- >>> :set -fplugin=Polysemy.Plugin -- >>> :set -fplugin=Polysemy.Plugin
-- >>> :m +Polysemy -- >>> :m +Polysemy
-- >>> :m +Polysemy.State -- >>> :m +Polysemy.State
-- >>> :m +Polysemy.Reader
-- >>> :m +Data.Maybe -- >>> :m +Data.Maybe
@ -26,3 +27,50 @@ module TypeErrors where
-- ... -- ...
missingFmap = () missingFmap = ()
--------------------------------------------------------------------------------
-- |
-- >>> :{
-- insertSome :: ∀ e1 e2 s r a . Sem (e1 ': e2 ': r) a -> Sem (e1 ': e2 ': State s ': r) a
-- insertSome = insertAt
-- :}
-- ...
-- ... insertAt: You must provide the index ...
-- ...
insertAtUnprovidedIndex = ()
--------------------------------------------------------------------------------
-- |
-- >>> :{
-- insertSome :: Sem (e1 : e2 : e3 : e4 : r) a -> Sem (e1 : e2 : Reader i : e3 : e4 : r) a
-- insertSome = insertAt @1
-- :}
-- ...
-- ...insertAt: Failed to insert effects at index 1
-- ...
-- ... before ...
-- ...'[e1]
-- ... after ...
-- ...e2 : e3 : e4 : r
-- ...Actual ...
-- ...e1 : e2 : Reader i : e3 : e4 : r
-- ...
insertAtWrongIndex = ()
--------------------------------------------------------------------------------
-- |
-- Note: The /Actual/ row doesn't match the return type because of the 'raiseUnder'.
-- >>> :{
-- insertSome :: Sem (e1 : r) a -> Sem (e1 : e2 : State s : e3 : Reader i : e4 : r) a
-- insertSome = raiseUnder . insertAt @2 . raise2Under
-- :}
-- ...
-- ...insertAt: Failed to insert effects at index 2
-- ...
-- ... before ...
-- ...'[e1, State s]
-- ... after ...
-- ...e30 : r0
-- ...Actual ...
-- ...e1 : State s : e3 : Reader i : e4 : r
-- ...
insertAtAndRaiseWrongIndex = ()

View File

@ -62,8 +62,10 @@ library
Polysemy.Internal.CustomErrors.Redefined Polysemy.Internal.CustomErrors.Redefined
Polysemy.Internal.Fixpoint Polysemy.Internal.Fixpoint
Polysemy.Internal.Forklift Polysemy.Internal.Forklift
Polysemy.Internal.Index
Polysemy.Internal.Kind Polysemy.Internal.Kind
Polysemy.Internal.NonDet Polysemy.Internal.NonDet
Polysemy.Internal.Sing
Polysemy.Internal.Strategy Polysemy.Internal.Strategy
Polysemy.Internal.Tactics Polysemy.Internal.Tactics
Polysemy.Internal.TH.Common Polysemy.Internal.TH.Common

View File

@ -35,6 +35,7 @@ module Polysemy
, raise3Under , raise3Under
, raise_ , raise_
, subsume_ , subsume_
, insertAt
-- * Trivial Interpretation -- * Trivial Interpretation
, subsume , subsume

View File

@ -9,13 +9,13 @@ module Polysemy.Bundle
, runBundle , runBundle
, subsumeBundle , subsumeBundle
-- * Miscellaneous -- * Miscellaneous
, KnownList
) where ) where
import Polysemy import Polysemy
import Polysemy.Internal import Polysemy.Internal
import Polysemy.Internal.Bundle
import Polysemy.Internal.Union import Polysemy.Internal.Union
import Polysemy.Internal.Bundle (subsumeMembership)
import Polysemy.Internal.Sing (KnownList (singList))
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- | An effect for collecting multiple effects into one effect. -- | An effect for collecting multiple effects into one effect.
@ -58,8 +58,8 @@ runBundle
-> Sem (Append r' r) a -> Sem (Append r' r) a
runBundle = hoistSem $ \u -> hoist runBundle $ case decomp u of runBundle = hoistSem $ \u -> hoist runBundle $ case decomp u of
Right (Weaving (Bundle pr e) s wv ex ins) -> Right (Weaving (Bundle pr e) s wv ex ins) ->
Union (extendMembership @_ @r pr) $ Weaving e s wv ex ins Union (extendMembershipRight @r' @r pr) $ Weaving e s wv ex ins
Left g -> weakenList @r' @r g Left g -> weakenList @r' @r (singList @r') g
{-# INLINE runBundle #-} {-# INLINE runBundle #-}
------------------------------------------------------------------------------ ------------------------------------------------------------------------------

View File

@ -30,6 +30,7 @@ module Polysemy.Internal
, Subsume (..) , Subsume (..)
, subsume , subsume
, subsumeUsing , subsumeUsing
, insertAt
, Embed (..) , Embed (..)
, usingSem , usingSem
, liftSem , liftSem
@ -44,6 +45,8 @@ module Polysemy.Internal
import Control.Applicative import Control.Applicative
import Control.Monad import Control.Monad
#if __GLASGOW_HASKELL__ < 808 #if __GLASGOW_HASKELL__ < 808
import Control.Monad.Fail import Control.Monad.Fail
#endif #endif
import Control.Monad.Fix import Control.Monad.Fix
@ -53,10 +56,13 @@ import Data.Kind
import Polysemy.Embed.Type import Polysemy.Embed.Type
import Polysemy.Fail.Type import Polysemy.Fail.Type
import Polysemy.Internal.Fixpoint import Polysemy.Internal.Fixpoint
import Polysemy.Internal.Index (InsertAtUnprovidedIndex, InsertAtIndex(insertAtIndex))
import Polysemy.Internal.Kind import Polysemy.Internal.Kind
import Polysemy.Internal.NonDet import Polysemy.Internal.NonDet
import Polysemy.Internal.PluginLookup import Polysemy.Internal.PluginLookup
import Polysemy.Internal.Union import Polysemy.Internal.Union
import Type.Errors (WhenStuck)
import Polysemy.Internal.Sing (ListOfLength (listOfLength))
-- $setup -- $setup
@ -540,6 +546,32 @@ subsumeUsing pr =
go go
{-# INLINE subsumeUsing #-} {-# INLINE subsumeUsing #-}
------------------------------------------------------------------------------
-- | Introduce a set of effects into 'Sem' at the index @i@, before the effect
-- that previously occupied that position. This is intended to be used with a
-- type application:
--
-- @
-- let
-- sem1 :: Sem [e1, e2, e3, e4, e5] a
-- sem1 = insertAt @2 (sem0 :: Sem [e1, e2, e5] a)
-- @
--
-- @since 1.6.0.0
insertAt
:: forall index inserted head oldTail tail old full a
. ( ListOfLength index head
, WhenStuck index InsertAtUnprovidedIndex
, old ~ Append head oldTail
, tail ~ Append inserted oldTail
, full ~ Append head tail
, InsertAtIndex index head tail oldTail full inserted)
=> Sem old a
-> Sem full a
insertAt = hoistSem $ \u -> hoist (insertAt @index @inserted @head @oldTail) $
weakenMid @oldTail (listOfLength @index @head) (insertAtIndex @Effect @index @head @tail @oldTail @full @inserted) u
{-# INLINE insertAt #-}
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- | Embed an effect into a 'Sem'. This is used primarily via -- | Embed an effect into a 'Sem'. This is used primarily via
@ -588,11 +620,6 @@ runM (Sem m) = m $ \z ->
{-# INLINE runM #-} {-# INLINE runM #-}
type family Append l r where
Append (a ': l) r = a ': (Append l r)
Append '[] r = r
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- | Type synonym for interpreters that consume an effect without changing the -- | Type synonym for interpreters that consume an effect without changing the
-- return value. Offered for user convenience. -- return value. Offered for user convenience.

View File

@ -2,15 +2,11 @@
{-# OPTIONS_HADDOCK not-home #-} {-# OPTIONS_HADDOCK not-home #-}
module Polysemy.Internal.Bundle ( module Polysemy.Internal.Bundle where
module Polysemy.Internal.Bundle,
Append,
) where
import Data.Proxy import Polysemy (Members)
import Polysemy import Polysemy.Internal.Union (ElemOf(..), membership)
import Polysemy.Internal (Append) import Polysemy.Internal.Kind (Append)
import Polysemy.Internal.Union
extendMembership :: forall r r' e. ElemOf e r -> ElemOf e (Append r r') extendMembership :: forall r r' e. ElemOf e r -> ElemOf e (Append r r')
extendMembership Here = Here extendMembership Here = Here
@ -21,35 +17,3 @@ subsumeMembership :: forall r r' e. Members r r' => ElemOf e r -> ElemOf e r'
subsumeMembership Here = membership @e @r' subsumeMembership Here = membership @e @r'
subsumeMembership (There (pr :: ElemOf e r'')) = subsumeMembership @r'' @r' pr subsumeMembership (There (pr :: ElemOf e r'')) = subsumeMembership @r'' @r' pr
{-# INLINE subsumeMembership #-} {-# INLINE subsumeMembership #-}
weakenList :: forall r' r m a
. KnownList r'
=> Union r m a
-> Union (Append r' r) m a
weakenList u = unconsKnownList @_ @r' u (\_ (_ :: Proxy r'') -> weaken (weakenList @r'' u))
{-# INLINE weakenList #-}
------------------------------------------------------------------------------
-- | A class for type-level lists with a known spine.
--
-- This constraint is eventually satisfied as @r@ is instantied to a
-- concrete list.
class KnownList (l :: [k]) where
unconsKnownList :: (l ~ '[] => a)
-> ( forall x r
. (l ~ (x ': r), KnownList r)
=> Proxy x
-> Proxy r
-> a
)
-> a
instance KnownList '[] where
unconsKnownList b _ = b
{-# INLINE unconsKnownList #-}
instance KnownList r => KnownList (x ': r) where
unconsKnownList _ b = b Proxy Proxy
{-# INLINE unconsKnownList #-}

View File

@ -0,0 +1,55 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# OPTIONS_HADDOCK not-home #-}
module Polysemy.Internal.Index where
import GHC.TypeLits (Nat)
import Type.Errors (TypeError, ErrorMessage(ShowType))
import Type.Errors.Pretty (type (<>), type (%))
import Polysemy.Internal.Sing (SList (SEnd, SCons))
------------------------------------------------------------------------------
-- | Infer a partition of the result type @full@ so that for the fixed segments
-- @head@ and @tail@, the new segment @inserted@ contains the missing effects
-- between them.
class InsertAtIndex (index :: Nat) (head :: [k]) (tail :: [k]) (oldTail :: [k]) (full :: [k]) (inserted :: [k]) where
insertAtIndex :: SList inserted
instance inserted ~ '[] => InsertAtIndex index head oldTail oldTail full inserted where
insertAtIndex = SEnd
{-# INLINE insertAtIndex #-}
instance {-# INCOHERENT #-} (
InsertAtIndex index head tail oldTail full insertedTail,
inserted ~ (e ': insertedTail)
) => InsertAtIndex index head (e ': tail) oldTail full inserted where
insertAtIndex = SCons (insertAtIndex @_ @index @head @tail @oldTail @full)
{-# INLINE insertAtIndex #-}
instance {-# INCOHERENT #-} TypeError (InsertAtFailure index oldTail head full)
=> InsertAtIndex index head tail oldTail full inserted where
insertAtIndex = error "unreachable"
type family InsertAtUnprovidedIndex where
InsertAtUnprovidedIndex = TypeError (
"insertAt: You must provide the index at which the effects should be inserted as a type application."
% "Example: insertAt @5"
)
type InsertAtFailure index soughtTail head full =
"insertAt: Failed to insert effects at index " <> 'ShowType index
% "There is a mismatch between what's been determined as the head and tail between the newly inserted effects,"
<> " and the actual desired return type."
% "Determined head before inserted effects:"
% "\t" <> 'ShowType head
% "Determined tail after the inserted effects:"
% "\t" <> 'ShowType soughtTail
% "Actual desired return type:"
% "\t" <> 'ShowType full
% "Make sure that the index provided to insertAt is correct, and that the desired return type simply requires"
<> " inserting effects."

View File

@ -1,6 +1,8 @@
{-# OPTIONS_HADDOCK not-home #-}
module Polysemy.Internal.Kind where module Polysemy.Internal.Kind where
import Data.Kind import Data.Kind (Type)
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- | The kind of effects. -- | The kind of effects.
@ -14,3 +16,7 @@ type Effect = (Type -> Type) -> Type -> Type
-- @since 0.5.0.0 -- @since 0.5.0.0
type EffectRow = [Effect] type EffectRow = [Effect]
type family Append l r where
Append (a ': l) r = a ': (Append l r)
Append '[] r = r

View File

@ -0,0 +1,37 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_HADDOCK not-home #-}
module Polysemy.Internal.Sing where
import GHC.TypeLits (type (-), Nat)
import Polysemy.Internal.Kind (Effect)
data SList l where
SEnd :: SList '[]
SCons :: SList xs -> SList (x ': xs)
class KnownList l where
singList :: SList l
instance KnownList '[] where
singList = SEnd
{-# INLINE singList #-}
instance KnownList xs => KnownList (x ': xs) where
singList = SCons singList
{-# INLINE singList #-}
class ListOfLength (n :: Nat) (l :: [Effect]) where
listOfLength :: SList l
instance {-# OVERLAPPING #-} (l ~ '[]) => ListOfLength 0 l where
listOfLength = SEnd
{-# INLINE listOfLength #-}
instance (ListOfLength (n - 1) xs, l ~ (x ': xs)) => ListOfLength n l where
listOfLength = SCons (listOfLength @(n - 1))
{-# INLINE listOfLength #-}

View File

@ -39,7 +39,11 @@ module Polysemy.Internal.Union
-- * Checking membership -- * Checking membership
, KnownRow , KnownRow
, tryMembership , tryMembership
) where , extendMembershipLeft
, extendMembershipRight
, injectMembership
, weakenList
, weakenMid) where
import Control.Monad import Control.Monad
import Data.Functor.Compose import Data.Functor.Compose
@ -48,6 +52,7 @@ import Data.Kind
import Data.Typeable import Data.Typeable
import Polysemy.Internal.Kind import Polysemy.Internal.Kind
import {-# SOURCE #-} Polysemy.Internal import {-# SOURCE #-} Polysemy.Internal
import Polysemy.Internal.Sing (SList (SEnd, SCons))
#ifndef NO_ERROR_MESSAGES #ifndef NO_ERROR_MESSAGES
import Polysemy.Internal.CustomErrors import Polysemy.Internal.CustomErrors
@ -251,6 +256,41 @@ tryMembership :: forall e r. (Typeable e, KnownRow r) => Maybe (ElemOf e r)
tryMembership = tryMembership' @r @e tryMembership = tryMembership' @r @e
{-# INLINE tryMembership #-} {-# INLINE tryMembership #-}
------------------------------------------------------------------------------
-- | Extends a proof that @e@ is an element of @r@ to a proof that @e@ is an
-- element of the concatenation of the lists @l@ and @r@.
-- @l@ must be specified as a singleton list proof.
extendMembershipLeft :: forall l r e. SList l -> ElemOf e r -> ElemOf e (Append l r)
extendMembershipLeft SEnd pr = pr
extendMembershipLeft (SCons l) pr = There (extendMembershipLeft l pr)
{-# INLINE extendMembershipLeft #-}
------------------------------------------------------------------------------
-- | Extends a proof that @e@ is an element of @l@ to a proof that @e@ is an
-- element of the concatenation of the lists @l@ and @r@.
extendMembershipRight :: forall l r e. ElemOf e l -> ElemOf e (Append l r)
extendMembershipRight Here = Here
extendMembershipRight (There e) = There (extendMembershipRight @_ @r e)
{-# INLINE extendMembershipRight #-}
------------------------------------------------------------------------------
-- | Extends a proof that @e@ is an element of @left <> right@ to a proof that
-- @e@ is an element of @left <> mid <> right@.
-- Both @left@ and @right@ must be specified as singleton list proofs.
injectMembership :: forall right e left mid
. SList left
-> SList mid
-> ElemOf e (Append left right)
-> ElemOf e (Append left (Append mid right))
injectMembership SEnd sm pr = extendMembershipLeft sm pr
injectMembership (SCons _) _ Here = Here
injectMembership (SCons sl) sm (There pr) = There (injectMembership @right sl sm pr)
{-# INLINE injectMembership #-}
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- | Decompose a 'Union'. Either this union contains an effect @e@---the head -- | Decompose a 'Union'. Either this union contains an effect @e@---the head
-- of the @r@ list---or it doesn't. -- of the @r@ list---or it doesn't.
@ -282,12 +322,32 @@ absurdU = \case {}
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- | Weaken a 'Union' so it is capable of storing a new sort of effect. -- | Weaken a 'Union' so it is capable of storing a new sort of effect at the
-- head.
weaken :: forall e r m a. Union r m a -> Union (e ': r) m a weaken :: forall e r m a. Union r m a -> Union (e ': r) m a
weaken (Union pr a) = Union (There pr) a weaken (Union pr a) = Union (There pr) a
{-# INLINE weaken #-} {-# INLINE weaken #-}
------------------------------------------------------------------------------
-- | Weaken a 'Union' so it is capable of storing a number of new effects at
-- the head, specified as a singleton list proof.
weakenList :: SList l -> Union r m a -> Union (Append l r) m a
weakenList sl (Union pr e) = Union (extendMembershipLeft sl pr) e
{-# INLINE weakenList #-}
------------------------------------------------------------------------------
-- | Weaken a 'Union' so it is capable of storing a number of new effects
-- somewhere within the previous effect list.
-- Both the prefix and the new effects are specified as singleton list proofs.
weakenMid :: forall right m a left mid
. SList left -> SList mid
-> Union (Append left right) m a
-> Union (Append left (Append mid right)) m a
weakenMid sl sm (Union pr e) = Union (injectMembership @right sl sm pr) e
{-# INLINE weakenMid #-}
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- | Lift an effect @e@ into a 'Union' capable of holding it. -- | Lift an effect @e@ into a 'Union' capable of holding it.