Membership proof rewrite, membership testing, Bundle effect (#282)

* Union rewrite

* Exports, tests, and renamed KnownEffectRow

* Got rid of artifacts accidently introduced

* More documentation. tryMembership seperate from KnownRow

* 'expose' combinator

* Applied review suggestions, add Membership module

* Fixed a replace-all goof

* Scrap expose/Using in favor of interceptUsing/H

* Fixed Haddock failure
This commit is contained in:
KingoftheHomeless 2019-12-08 13:52:37 +01:00 committed by GitHub
parent fc508de2ec
commit a4868bddd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 435 additions and 94 deletions

View File

@ -76,7 +76,7 @@ data FindConstraint = FindConstraint
-- | Given a list of constraints, filter out the 'FindConstraint's.
getFindConstraints :: PolysemyStuff 'Things -> [Ct] -> [FindConstraint]
getFindConstraints (findClass -> cls) cts = do
cd@CDictCan{cc_class = cls', cc_tyargs = [_, r, eff]} <- cts
cd@CDictCan{cc_class = cls', cc_tyargs = [_, eff, r]} <- cts
guard $ cls == cls'
pure $ FindConstraint
{ fcLoc = ctLoc cd
@ -163,7 +163,7 @@ extractRowFromSem (semTyCon -> sem) ty = do
------------------------------------------------------------------------------
-- | Given a list of bogus @r@s, and the wanted constraints, produce bogus
-- evidence terms that will prevent @IfStuck (IndexOf r _) _ _@ error messsages.
-- evidence terms that will prevent @IfStuck (LocateEffect _ r) _ _@ error messsages.
solveBogusError :: PolysemyStuff 'Things -> [Ct] -> [(EvTerm, Ct)]
solveBogusError stuff wanteds = do
let splitTyConApp_list = maybeToList . splitTyConApp_maybe
@ -172,8 +172,8 @@ solveBogusError stuff wanteds = do
ct@(CIrredCan ce _) <- wanteds
(stuck, [_, _, expr, _, _]) <- splitTyConApp_list $ ctev_pred ce
guard $ stuck == ifStuckTyCon stuff
(idx, [_, r, _]) <- splitTyConApp_list expr
guard $ idx == indexOfTyCon stuff
(idx, [_, _, r]) <- splitTyConApp_list expr
guard $ idx == locateEffectTyCon stuff
guard $ elem @[] (OrdType r) $ coerce bogus
pure (error "bogus proof for stuck type family", ct)

View File

@ -23,10 +23,10 @@ import Outputable (pprPanic, empty, text, (<+>), ($$))
-- When @l ~ 'Locations@, each of these is just a pair of strings. When @l
-- ~ 'Things@, it's actually references to the stuff.
data PolysemyStuff (l :: LookupState) = PolysemyStuff
{ findClass :: ThingOf l Class
, semTyCon :: ThingOf l TyCon
, ifStuckTyCon :: ThingOf l TyCon
, indexOfTyCon :: ThingOf l TyCon
{ findClass :: ThingOf l Class
, semTyCon :: ThingOf l TyCon
, ifStuckTyCon :: ThingOf l TyCon
, locateEffectTyCon :: ThingOf l TyCon
}
@ -34,10 +34,10 @@ data PolysemyStuff (l :: LookupState) = PolysemyStuff
-- | All of the things we need to lookup.
polysemyStuffLocations :: PolysemyStuff 'Locations
polysemyStuffLocations = PolysemyStuff
{ findClass = ("Polysemy.Internal.Union", "Find")
, semTyCon = ("Polysemy.Internal", "Sem")
, ifStuckTyCon = ("Polysemy.Internal.CustomErrors.Redefined", "IfStuck")
, indexOfTyCon = ("Polysemy.Internal.Union", "IndexOf")
{ findClass = ("Polysemy.Internal.Union", "Find")
, semTyCon = ("Polysemy.Internal", "Sem")
, ifStuckTyCon = ("Polysemy.Internal.CustomErrors.Redefined", "IfStuck")
, locateEffectTyCon = ("Polysemy.Internal.Union", "LocateEffect")
}

View File

@ -4,7 +4,7 @@ cabal-version: 1.24
--
-- see: https://github.com/sol/hpack
--
-- hash: aac2227e71e3a98458fa5746be7fcbb87da0dc342cdd53913041c95c128871fe
-- hash: 582807c5d69a34a9e991a77c6111532c05dbb54a5d7aa6662267b7af2d3047ee
name: polysemy
version: 1.2.3.0
@ -48,6 +48,7 @@ library
Polysemy
Polysemy.Async
Polysemy.AtomicState
Polysemy.Bundle
Polysemy.Embed
Polysemy.Embed.Type
Polysemy.Error
@ -57,6 +58,7 @@ library
Polysemy.Fixpoint
Polysemy.Input
Polysemy.Internal
Polysemy.Internal.Bundle
Polysemy.Internal.Combinators
Polysemy.Internal.CustomErrors
Polysemy.Internal.CustomErrors.Redefined
@ -72,6 +74,7 @@ library
Polysemy.Internal.Writer
Polysemy.IO
Polysemy.Law
Polysemy.Membership
Polysemy.NonDet
Polysemy.Output
Polysemy.Reader
@ -134,6 +137,7 @@ test-suite polysemy-test
HigherOrderSpec
InspectorSpec
InterceptSpec
KnownRowSpec
LawsSpec
OutputSpec
ThEffectSpec

76
src/Polysemy/Bundle.hs Normal file
View File

@ -0,0 +1,76 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
module Polysemy.Bundle
( -- * Effect
Bundle (..)
-- * Actions
, sendBundle
, injBundle
-- * Interpretations
, runBundle
, subsumeBundle
-- * Miscellaneous
, KnownList
) where
import Polysemy
import Polysemy.Internal
import Polysemy.Internal.Bundle
import Polysemy.Internal.Union
------------------------------------------------------------------------------
-- | An effect for collecting multiple effects into one effect.
--
-- Useful for effect newtypes -- effects defined through creating a newtype
-- over an existing effect, and then defining actions and interpretations on
-- the newtype by using 'rewrite' and 'transform'.
--
-- By making a newtype of 'Bundle', it's possible to wrap multiple effects in
-- one newtype.
data Bundle r m a where
Bundle :: ElemOf e r -> e m a -> Bundle r m a
------------------------------------------------------------------------------
-- | Injects an effect into a 'Bundle'. Useful together with 'transform'.
injBundle :: forall e r m a. Member e r => e m a -> Bundle r m a
injBundle = Bundle membership
{-# INLINE injBundle #-}
------------------------------------------------------------------------------
-- | Send uses of an effect to a 'Bundle' containing that effect.
sendBundle
:: forall e r' r a
. (Member e r', Member (Bundle r') r)
=> Sem (e ': r) a
-> Sem r a
sendBundle = hoistSem $ \u -> case decomp u of
Right (Weaving e s wv ex ins) ->
injWeaving $
Weaving (Bundle (membership @e @r') e) s (sendBundle @e @r' . wv) ex ins
Left g -> hoist (sendBundle @e @r') g
{-# INLINE sendBundle #-}
------------------------------------------------------------------------------
-- | Run a @'Bundle' r@ by prepending @r@ to the effect stack.
runBundle
:: forall r' r a
. KnownList r'
=> Sem (Bundle r' ': r) a
-> Sem (Append r' r) a
runBundle = hoistSem $ \u -> hoist runBundle $ case decomp u of
Right (Weaving (Bundle pr e) s wv ex ins) ->
Union (extendMembership @_ @r pr) $ Weaving e s wv ex ins
Left g -> weakenList @r' @r g
{-# INLINE runBundle #-}
------------------------------------------------------------------------------
-- | Run a @'Bundle' r@ if the effect stack contains all effects of @r@.
subsumeBundle
:: forall r' r a
. Members r' r
=> Sem (Bundle r' ': r) a
-> Sem r a
subsumeBundle = hoistSem $ \u -> hoist subsumeBundle $ case decomp u of
Right (Weaving (Bundle pr e) s wv ex ins) ->
Union (subsumeMembership pr) (Weaving e s wv ex ins)
Left g -> g
{-# INLINE subsumeBundle #-}

View File

@ -12,6 +12,7 @@ module Polysemy.Internal
, MemberWithError
, Members
, send
, sendUsing
, embed
, run
, runM
@ -20,6 +21,7 @@ module Polysemy.Internal
, raiseUnder2
, raiseUnder3
, subsume
, subsumeUsing
, Embed (..)
, usingSem
, liftSem
@ -351,8 +353,8 @@ raiseUnder :: ∀ e2 e1 r a. Sem (e1 ': r) a -> Sem (e1 ': e2 ': r) a
raiseUnder = hoistSem $ hoist raiseUnder . weakenUnder
where
weakenUnder :: m x. Union (e1 ': r) m x -> Union (e1 ': e2 ': r) m x
weakenUnder (Union SZ a) = Union SZ a
weakenUnder (Union (SS n) a) = Union (SS (SS n)) a
weakenUnder (Union Here a) = Union Here a
weakenUnder (Union (There n) a) = Union (There (There n)) a
{-# INLINE weakenUnder #-}
{-# INLINE raiseUnder #-}
@ -366,14 +368,14 @@ raiseUnder2 :: ∀ e2 e3 e1 r a. Sem (e1 ': r) a -> Sem (e1 ': e2 ': e3 ': r) a
raiseUnder2 = hoistSem $ hoist raiseUnder2 . weakenUnder2
where
weakenUnder2 :: m x. Union (e1 ': r) m x -> Union (e1 ': e2 ': e3 ': r) m x
weakenUnder2 (Union SZ a) = Union SZ a
weakenUnder2 (Union (SS n) a) = Union (SS (SS (SS n))) a
weakenUnder2 (Union Here a) = Union Here a
weakenUnder2 (Union (There n) a) = Union (There (There (There n))) a
{-# INLINE weakenUnder2 #-}
{-# INLINE raiseUnder2 #-}
------------------------------------------------------------------------------
-- | Like 'raise', but introduces two new effects underneath the head of the
-- | Like 'raise', but introduces three new effects underneath the head of the
-- list.
--
-- @since 1.2.0.0
@ -381,8 +383,8 @@ raiseUnder3 :: ∀ e2 e3 e4 e1 r a. Sem (e1 ': r) a -> Sem (e1 ': e2 ': e3 ': e4
raiseUnder3 = hoistSem $ hoist raiseUnder3 . weakenUnder3
where
weakenUnder3 :: m x. Union (e1 ': r) m x -> Union (e1 ': e2 ': e3 ': e4 ': r) m x
weakenUnder3 (Union SZ a) = Union SZ a
weakenUnder3 (Union (SS n) a) = Union (SS (SS (SS (SS n)))) a
weakenUnder3 (Union Here a) = Union Here a
weakenUnder3 (Union (There n) a) = Union (There (There (There (There n)))) a
{-# INLINE weakenUnder3 #-}
{-# INLINE raiseUnder3 #-}
@ -396,11 +398,37 @@ raiseUnder3 = hoistSem $ hoist raiseUnder3 . weakenUnder3
--
-- @since 1.2.0.0
subsume :: Member e r => Sem (e ': r) a -> Sem r a
subsume = hoistSem $ \u -> hoist subsume $ case decomp u of
Right w -> injWeaving w
Left g -> g
subsume = subsumeUsing membership
{-# INLINE subsume #-}
------------------------------------------------------------------------------
-- | Interprets an effect in terms of another identical effect, given an
-- explicit proof that the effect exists in @r@.
--
-- This is useful in conjunction with 'Polysemy.Membership.tryMembership'
-- in order to conditionally make use of effects. For example:
--
-- @
-- tryListen :: 'Polysemy.Membership.KnownRow' r => 'Sem' r a -> Maybe ('Sem' r ([Int], a))
-- tryListen m = case 'Polysemy.Membership.tryMembership' @('Polysemy.Writer.Writer' [Int]) of
-- Just pr -> Just $ 'subsumeUsing' pr ('Polysemy.Writer.listen' ('raise' m))
-- _ -> Nothing
-- @
--
-- @since TODO
subsumeUsing :: forall e r a. ElemOf e r -> Sem (e ': r) a -> Sem r a
subsumeUsing pr =
let
go :: forall x. Sem (e ': r) x -> Sem r x
go = hoistSem $ \u -> hoist go $ case decomp u of
Right w -> Union pr w
Left g -> g
{-# INLINE go #-}
in
go
{-# INLINE subsumeUsing #-}
------------------------------------------------------------------------------
-- | Embed an effect into a 'Sem'. This is used primarily via
-- 'Polysemy.makeSem' to implement smart constructors.
@ -408,6 +436,16 @@ send :: Member e r => e (Sem r) a -> Sem r a
send = liftSem . inj
{-# INLINE[3] send #-}
------------------------------------------------------------------------------
-- | Embed an effect into a 'Sem', given an explicit proof
-- that the effect exists in @r@.
--
-- This is useful in conjunction with 'Polysemy.Membership.tryMembership',
-- in order to conditionally make use of effects.
sendUsing :: ElemOf e r -> e (Sem r) a -> Sem r a
sendUsing pr = liftSem . injUsing pr
{-# INLINE[3] sendUsing #-}
------------------------------------------------------------------------------
-- | Embed a monadic action @m@ in 'Sem'.

View File

@ -0,0 +1,55 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_HADDOCK not-home #-}
module Polysemy.Internal.Bundle where
import Data.Proxy
import Polysemy
import Polysemy.Internal.Union
type family Append l r where
Append (a ': l) r = a ': (Append l r)
Append '[] r = r
extendMembership :: forall r r' e. ElemOf e r -> ElemOf e (Append r r')
extendMembership Here = Here
extendMembership (There e) = There (extendMembership @_ @r' e)
{-# INLINE extendMembership #-}
subsumeMembership :: forall r r' e. Members r r' => ElemOf e r -> ElemOf e r'
subsumeMembership Here = membership @e @r'
subsumeMembership (There (pr :: ElemOf e r'')) = subsumeMembership @r'' @r' pr
{-# 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

@ -19,6 +19,10 @@ module Polysemy.Internal.Combinators
, reinterpret2H
, reinterpret3H
-- * Conditional
, interceptUsing
, interceptUsingH
-- * Statefulness
, stateful
, lazilyStateful
@ -281,13 +285,55 @@ interceptH
-> Sem r a
-- ^ Unlike 'interpretH', 'interceptH' does not consume any effects.
-> Sem r a
interceptH f (Sem m) = Sem $ \k -> m $ \u ->
case prj u of
Just (Weaving e s d y v) ->
usingSem k $ fmap y $ runTactics s (raise . d) v $ f e
Nothing -> k $ hoist (interceptH f) u
interceptH = interceptUsingH membership
{-# INLINE interceptH #-}
------------------------------------------------------------------------------
-- | A variant of 'intercept' that accepts an explicit proof that the effect
-- is in the effect stack rather then requiring a 'Member' constraint.
--
-- This is useful in conjunction with 'Polysemy.Membership.tryMembership'
-- in order to conditionally perform 'intercept'.
interceptUsing
:: FirstOrder e "interceptUsing"
=> ElemOf e r
-- ^ A proof that the handled effect exists in @r@.
-- This can be retrieved through 'Polysemy.Membership.membership' or
-- 'Polysemy.Membership.tryMembership'.
-> ( x m. e m x -> Sem r x)
-- ^ A natural transformation from the handled effect to other effects
-- already in 'Sem'.
-> Sem r a
-- ^ Unlike 'interpret', 'intercept' does not consume any effects.
-> Sem r a
interceptUsing pr f = interceptUsingH pr $ \(e :: e m x) -> liftT @m $ f e
{-# INLINE interceptUsing #-}
------------------------------------------------------------------------------
-- | A variant of 'interceptH' that accepts an explicit proof that the effect
-- is in the effect stack rather then requiring a 'Member' constraint.
--
-- This is useful in conjunction with 'Polysemy.Membership.tryMembership'
-- in order to conditionally perform 'interceptH'.
--
-- See the notes on 'Tactical' for how to use this function.
interceptUsingH
:: ElemOf e r
-- ^ A proof that the handled effect exists in @r@.
-- This can be retrieved through 'Polysemy.Membership.membership' or
-- 'Polysemy.Membership.tryMembership'.
-> ( x m. e m x -> Tactical e m r x)
-- ^ A natural transformation from the handled effect to other effects
-- already in 'Sem'.
-> Sem r a
-- ^ Unlike 'interpretH', 'interceptUsingH' does not consume any effects.
-> Sem r a
interceptUsingH pr f (Sem m) = Sem $ \k -> m $ \u ->
case prjUsing pr u of
Just (Weaving e s d y v) ->
usingSem k $ fmap y $ runTactics s (raise . d) v $ f e
Nothing -> k $ hoist (interceptUsingH pr f) u
{-# INLINE interceptUsingH #-}
------------------------------------------------------------------------------
-- | Rewrite an effect @e1@ directly into @e2@, and put it on the top of the
@ -302,7 +348,7 @@ rewrite
rewrite f (Sem m) = Sem $ \k -> m $ \u ->
k $ hoist (rewrite f) $ case decompCoerce u of
Left x -> x
Right (Weaving e s d n y) -> injWeaving $ Weaving (f e) s d n y
Right (Weaving e s d n y) -> Union Here $ Weaving (f e) s d n y
------------------------------------------------------------------------------

View File

@ -76,7 +76,7 @@ type AmbigousEffectMessage (rstate :: EffectRowCtor)
% " " <> PrettyPrintList vs <> " directly, or activate polysemy-plugin which"
% " can usually infer the type correctly."
type AmbiguousSend r e =
type AmbiguousSend e r =
(IfStuck r
(AmbiguousSendError 'TyVarR r e)
(Pure (AmbiguousSendError (UnstuckRState r) r e)))

View File

@ -100,7 +100,7 @@ getInitialStateT = send @(Tactics _ m (e ': r)) GetInitialState
--
-- ==== Example
--
-- We can use the result of 'getInspectT' to "undo" 'pureT' (or any of the other
-- We can use the result of 'getInspectorT' to "undo" 'pureT' (or any of the other
-- 'Tactical' functions):
--
-- @

View File

@ -1,8 +1,10 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE EmptyCase #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeFamilies #-}
@ -20,24 +22,30 @@ module Polysemy.Internal.Union
, hoist
-- * Building Unions
, inj
, injUsing
, injWeaving
, weaken
-- * Using Unions
, decomp
, prj
, prjUsing
, extract
, absurdU
, decompCoerce
-- * Witnesses
, SNat (..)
, Nat (..)
, ElemOf (..)
, membership
, sameMember
-- * Checking membership
, KnownRow
, tryMembership
) where
import Control.Monad
import Data.Functor.Compose
import Data.Functor.Identity
import Data.Kind
import Data.Type.Equality
import Data.Typeable
import Polysemy.Internal.Kind
#ifndef NO_ERROR_MESSAGES
@ -51,10 +59,10 @@ import Polysemy.Internal.CustomErrors
data Union (r :: EffectRow) (m :: Type -> Type) a where
Union
:: -- A proof that the effect is actually in @r@.
SNat n
ElemOf e r
-- The effect to wrap. The functions 'prj' and 'decomp' can help
-- retrieve this value later.
-> Weaving (IndexOf r n) m a
-> Weaving e m a
-> Union r m a
instance Functor (Union r m) where
@ -129,74 +137,118 @@ type Member e r = MemberNoError e r
------------------------------------------------------------------------------
-- | Like 'Member', but will produce an error message if the types are
-- ambiguous.
-- ambiguous. This is the constraint used for actions generated by
-- 'Polysemy.makeSem'.
--
-- @since TODO
-- /Be careful with this./ Due to quirks of 'GHC.TypeLits.TypeError',
-- the custom error messages emitted by this can potentially override other,
-- more helpful error messages.
-- See the discussion in
-- <https://github.com/polysemy-research/polysemy/issues/227 Issue #227>.
--
-- @since 1.2.3.0
type MemberWithError e r =
( MemberNoError e r
#ifndef NO_ERROR_MESSAGES
-- NOTE: The plugin explicitly pattern matches on
-- `WhenStuck (IndexOf r _) _`, so if you change this, make sure to change
-- `WhenStuck (LocateEffect _ r) _`, so if you change this, make sure to change
-- the corresponding implementation in
-- Polysemy.Plugin.Fundep.solveBogusError
, WhenStuck (IndexOf r (Found r e)) (AmbiguousSend r e)
, WhenStuck (LocateEffect e r) (AmbiguousSend e r)
#endif
)
type MemberNoError e r =
( Find r e
, e ~ IndexOf r (Found r e)
( Find e r
#ifndef NO_ERROR_MESSAGES
, LocateEffect e r ~ '()
#endif
)
------------------------------------------------------------------------------
-- | A proof that @e@ is an element of @r@.
--
-- Due to technical reasons, @'ElemOf' e r@ is not powerful enough to
-- prove @'Member' e r@; however, it can still be used send actions of @e@
-- into @r@ by using 'Polysemy.Internal.subsumeUsing'.
data ElemOf e r where
-- | @e@ is located at the head of the list.
Here :: ElemOf e (e ': r)
-- | @e@ is located somewhere in the tail of the list.
There :: ElemOf e r -> ElemOf e (e' ': r)
------------------------------------------------------------------------------
-- | The kind of type-level natural numbers.
data Nat = Z | S Nat
-- | Checks if two membership proofs are equal. If they are, then that means
-- that the effects for which membership is proven must also be equal.
sameMember :: forall e e' r
. ElemOf e r
-> ElemOf e' r
-> Maybe (e :~: e')
sameMember Here Here =
Just Refl
sameMember (There pr) (There pr') =
sameMember @e @e' pr pr'
sameMember (There _) _ =
Nothing
sameMember _ _ =
Nothing
------------------------------------------------------------------------------
-- | A singleton for 'Nat'.
data SNat :: Nat -> Type where
SZ :: SNat 'Z
SS :: SNat n -> SNat ('S n)
instance TestEquality SNat where
testEquality SZ SZ = Just Refl
testEquality (SS _) SZ = Nothing
testEquality SZ (SS _) = Nothing
testEquality (SS n) (SS m) =
case testEquality n m of
Nothing -> Nothing
Just Refl -> Just Refl
{-# INLINE testEquality #-}
type family IndexOf (ts :: [k]) (n :: Nat) :: k where
IndexOf (k ': ks) 'Z = k
IndexOf (k ': ks) ('S n) = IndexOf ks n
type family Found (ts :: [k]) (t :: k) :: Nat where
-- | Used to detect ambiguous uses of effects. If @r@ isn't concrete,
-- and we haven't been given @'LocateEffect' e r ~ '()@ from a
-- @'Member' e r@ constraint, then @'LocateEffect' e r@ will get stuck.
type family LocateEffect (t :: k) (ts :: [k]) :: () where
#ifndef NO_ERROR_MESSAGES
Found '[] t = UnhandledEffect t
LocateEffect t '[] = UnhandledEffect t
#endif
Found (t ': ts) t = 'Z
Found (u ': ts) t = 'S (Found ts t)
LocateEffect t (t ': ts) = '()
LocateEffect t (u ': ts) = LocateEffect t ts
class Find (t :: k) (r :: [k]) where
membership' :: ElemOf t r
class Find (r :: [k]) (t :: k) where
finder :: SNat (Found r t)
instance {-# OVERLAPPING #-} Find t (t ': z) where
membership' = Here
{-# INLINE membership' #-}
instance {-# OVERLAPPING #-} Find (t ': z) t where
finder = SZ
{-# INLINE finder #-}
instance Find t z => Find t (_1 ': z) where
membership' = There $ membership' @_ @t @z
{-# INLINE membership' #-}
instance ( Find z t
, Found (_1 ': z) t ~ 'S (Found z t)
) => Find (_1 ': z) t where
finder = SS $ finder @_ @z @t
{-# INLINE finder #-}
------------------------------------------------------------------------------
-- | A class for effect rows whose elements are inspectable.
--
-- This constraint is eventually satisfied as @r@ is instantied to a
-- monomorphic list.
-- (E.g when @r@ becomes something like
-- @'['Polysemy.State.State' Int, 'Polysemy.Output.Output' String, 'Polysemy.Embed' IO]@)
class KnownRow r where
tryMembership' :: forall e. Typeable e => Maybe (ElemOf e r)
instance KnownRow '[] where
tryMembership' = Nothing
{-# INLINE tryMembership' #-}
instance (Typeable e, KnownRow r) => KnownRow (e ': r) where
tryMembership' :: forall e'. Typeable e' => Maybe (ElemOf e' (e ': r))
tryMembership' = case eqT @e @e' of
Just Refl -> Just Here
_ -> There <$> tryMembership' @r @e'
{-# INLINE tryMembership' #-}
------------------------------------------------------------------------------
-- | Given @'Member' e r@, extract a proof that @e@ is an element of @r@.
membership :: Member e r => ElemOf e r
membership = membership'
{-# INLINE membership #-}
------------------------------------------------------------------------------
-- | Extracts a proof that @e@ is an element of @r@ if that
-- is indeed the case; otherwise returns @Nothing@.
tryMembership :: forall e r. (Typeable e, KnownRow r) => Maybe (ElemOf e r)
tryMembership = tryMembership' @r @e
{-# INLINE tryMembership #-}
------------------------------------------------------------------------------
-- | Decompose a 'Union'. Either this union contains an effect @e@---the head
@ -204,29 +256,28 @@ instance ( Find z t
decomp :: Union (e ': r) m a -> Either (Union r m a) (Weaving e m a)
decomp (Union p a) =
case p of
SZ -> Right a
SS n -> Left $ Union n a
Here -> Right a
There pr -> Left $ Union pr a
{-# INLINE decomp #-}
------------------------------------------------------------------------------
-- | Retrieve the last effect in a 'Union'.
extract :: Union '[e] m a -> Weaving e m a
extract (Union SZ a) = a
extract _ = error "impossible"
extract (Union Here a) = a
extract (Union (There g) _) = case g of {}
{-# INLINE extract #-}
------------------------------------------------------------------------------
-- | An empty union contains nothing, so this function is uncallable.
absurdU :: Union '[] m a -> b
absurdU = absurdU
absurdU (Union pr _) = case pr of {}
------------------------------------------------------------------------------
-- | Weaken a 'Union' so it is capable of storing a new sort of effect.
weaken :: forall e r m a. Union r m a -> Union (e ': r) m a
weaken (Union n a) = Union (SS n) a
weaken (Union pr a) = Union (There pr) a
{-# INLINE weaken #-}
@ -241,10 +292,22 @@ inj e = injWeaving $
(Just . runIdentity)
{-# INLINE inj #-}
------------------------------------------------------------------------------
-- | Lift an effect @e@ into a 'Union' capable of holding it,
-- given an explicit proof that the effect exists in @r@
injUsing :: forall e r m a. Functor m => ElemOf e r -> e m a -> Union r m a
injUsing pr e = Union pr $
Weaving e (Identity ())
(fmap Identity . runIdentity)
runIdentity
(Just . runIdentity)
{-# INLINE injUsing #-}
------------------------------------------------------------------------------
-- | Lift a @'Weaving' e@ into a 'Union' capable of holding it.
injWeaving :: forall e r m a. Member e r => Weaving e m a -> Union r m a
injWeaving = Union (finder @_ @r @e)
injWeaving = Union membership
{-# INLINE injWeaving #-}
------------------------------------------------------------------------------
@ -254,12 +317,19 @@ prj :: forall e r m a
)
=> Union r m a
-> Maybe (Weaving e m a)
prj (Union sn a) =
case testEquality sn (finder @_ @r @e) of
Nothing -> Nothing
Just Refl -> Just a
prj = prjUsing membership
{-# INLINE prj #-}
------------------------------------------------------------------------------
-- | Attempt to take an @e@ effect out of a 'Union', given an explicit
-- proof that the effect exists in @r@.
prjUsing
:: forall e r m a
. ElemOf e r
-> Union r m a
-> Maybe (Weaving e m a)
prjUsing pr (Union sn a) = (\Refl -> a) <$> sameMember pr sn
{-# INLINE prjUsing #-}
------------------------------------------------------------------------------
-- | Like 'decomp', but allows for a more efficient
@ -269,6 +339,6 @@ decompCoerce
-> Either (Union (f ': r) m a) (Weaving e m a)
decompCoerce (Union p a) =
case p of
SZ -> Right a
SS n -> Left (Union (SS n) a)
Here -> Right a
There pr -> Left (Union (There pr) a)
{-# INLINE decompCoerce #-}

View File

@ -0,0 +1,17 @@
module Polysemy.Membership
( -- * Witnesses
ElemOf (..)
, membership
, sameMember
-- * Checking membership
, KnownRow
, tryMembership
-- * Using membership
, subsumeUsing
, interceptUsing
, interceptUsingH
) where
import Polysemy.Internal
import Polysemy.Internal.Combinators
import Polysemy.Internal.Union

View File

@ -80,7 +80,7 @@ untag
-- Once GHC 8.10 rolls out, I will benchmark and compare.
untag = hoistSem $ \u -> case decompCoerce u of
Right (Weaving (Tagged e) s wv ex ins) ->
Union SZ (Weaving e s (untag . wv) ex ins)
Union Here (Weaving e s (untag . wv) ex ins)
Left g -> hoist untag g
{-# INLINE untag #-}

35
test/KnownRowSpec.hs Normal file
View File

@ -0,0 +1,35 @@
module KnownRowSpec where
import Polysemy
import Polysemy.Error
import Polysemy.State
import Polysemy.Internal
import Polysemy.Internal.Union
import Test.Hspec
-- | A variant of 'runState' that uses 'stateToIO' if @r@ contains @Embed IO@.
-- (Can also be extended to check for @Final IO@)
runState' :: forall s r a. KnownRow r => s -> Sem (State s ': r) a -> Sem r (s, a)
runState' s sem = case tryMembership @(Embed IO) of
Just proof -> subsumeUsing proof (stateToIO s (raiseUnder sem))
_ -> runState s sem
test :: (Member (Error ()) r, KnownRow r)
=> Sem r String
test = fmap fst $ runState' "" $ do
put "local state"
_ <- (put "global state" >> throw ()) `catch` \() -> return ()
return ()
spec :: Spec
spec = parallel $ describe "tryMembership" $ do
it "should return a valid proof when the targeted \
\ effect is part of the row" $ do
res <- runM . runError @() $ test
res `shouldBe` Right "global state"
it "should not return a valid proof when the targeted \
\ effect is not part of the row" $ do
let res = run . runError @() $ test
res `shouldBe` Right "local state"