-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🔥 interposition #223
🔥 interposition #223
Changes from all commits
4b2d514
7279d50
e9c2a07
2e9831b
b85aa5a
9436c4b
a6335fc
bca585d
515bd62
6a58acc
85e8784
f38e047
e4121c1
2542577
7f4aff7
dc6e977
144b90b
76c6b50
51651c3
dc8c56a
e24df62
f4d017c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,16 @@ | ||
{-# LANGUAGE ConstraintKinds, DeriveGeneric, DeriveTraversable, FlexibleContexts, FlexibleInstances, KindSignatures, MultiParamTypeClasses, TypeOperators, UndecidableInstances #-} | ||
{-# LANGUAGE DeriveGeneric, DeriveTraversable, FlexibleContexts, FlexibleInstances, KindSignatures, MultiParamTypeClasses, TypeOperators, UndecidableInstances #-} | ||
-- | Operations on /sums/, combining effects into a /signature/. | ||
module Control.Effect.Sum | ||
( (:+:)(..) | ||
, Member | ||
, Inject(..) | ||
, Project(..) | ||
( -- * Membership | ||
Member(..) | ||
-- * Sums | ||
, (:+:)(..) | ||
) where | ||
|
||
import Control.Effect.Class | ||
import GHC.Generics (Generic1) | ||
|
||
-- | Higher-order sums are used to combine multiple effects into a signature, typically by chaining on the right. | ||
data (f :+: g) (m :: * -> *) k | ||
= L (f m k) | ||
| R (g m k) | ||
|
@@ -20,54 +22,35 @@ instance (HFunctor f, HFunctor g) => HFunctor (f :+: g) | |
instance (Effect f, Effect g) => Effect (f :+: g) | ||
|
||
|
||
type Member sub sup = (Inject sub sup, Project sub sup) | ||
|
||
|
||
class Inject (sub :: (* -> *) -> (* -> *)) sup where | ||
-- | The class of types present in a signature. | ||
-- | ||
-- This is based on Wouter Swierstra’s design described in [Data types à la carte](http://www.cs.ru.nl/~W.Swierstra/Publications/DataTypesALaCarte.pdf). As described therein, overlapping instances are required in order to distinguish e.g. left-occurrence from right-recursion. | ||
-- | ||
-- It should not generally be necessary for you to define new 'Member' instances, but these are not specifically prohibited if you wish to get creative. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per #219 (comment), I’ve documented the overall design, the overlapping instances, and the reassociation; I’ve also noted here that you probably don’t need to define your own instances. |
||
class Member (sub :: (* -> *) -> (* -> *)) sup where | ||
-- | Inject a member of a signature into the signature. | ||
inj :: sub m a -> sup m a | ||
|
||
instance Inject t t where | ||
-- | Reflexivity: @t@ is a member of itself. | ||
instance Member t t where | ||
inj = id | ||
|
||
-- | Left-recursion: if @t@ is a member of @l1 ':+:' l2 ':+:' r@, then we can inject it into @(l1 ':+:' l2) ':+:' r@ by injection into a right-recursive signature, followed by left-association. | ||
instance {-# OVERLAPPABLE #-} | ||
Inject t (l1 :+: l2 :+: r) | ||
=> Inject t ((l1 :+: l2) :+: r) where | ||
Member t (l1 :+: l2 :+: r) | ||
=> Member t ((l1 :+: l2) :+: r) where | ||
inj = reassoc . inj where | ||
reassoc (L l) = L (L l) | ||
reassoc (R (L l)) = L (R l) | ||
reassoc (R (R r)) = R r | ||
|
||
-- | Left-occurrence: if @t@ is at the head of a signature, we can inject it in O(1). | ||
instance {-# OVERLAPPABLE #-} | ||
Inject l (l :+: r) where | ||
Member l (l :+: r) where | ||
inj = L | ||
|
||
-- | Right-recursion: if @t@ is a member of @r@, we can inject it into @r@ in O(n), followed by lifting that into @l ':+:' r@ in O(1). | ||
instance {-# OVERLAPPABLE #-} | ||
Inject l r | ||
=> Inject l (l' :+: r) where | ||
Member l r | ||
=> Member l (l' :+: r) where | ||
inj = R . inj | ||
|
||
|
||
class Project (sub :: (* -> *) -> (* -> *)) sup where | ||
prj :: sup m a -> Maybe (sub m a) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We currently use The latter category make me wonder if we should retain this for syntax use cases, but tbh
This makes Thus, while I’m slightly concerned about this, I am currently, on the balance, convinced that removing it is right for the library; but I would nevertheless particularly welcome a gut check. |
||
|
||
instance Project t t where | ||
prj = Just | ||
|
||
instance {-# OVERLAPPABLE #-} | ||
Project t (l1 :+: l2 :+: r) | ||
=> Project t ((l1 :+: l2) :+: r) where | ||
prj = prj . reassoc where | ||
reassoc (L (L l)) = L l | ||
reassoc (L (R l)) = R (L l) | ||
reassoc (R r) = R (R r) | ||
|
||
instance {-# OVERLAPPABLE #-} | ||
Project l (l :+: r) where | ||
prj (L f) = Just f | ||
prj _ = Nothing | ||
|
||
instance {-# OVERLAPPABLE #-} | ||
Project l r | ||
=> Project l (l' :+: r) where | ||
prj (R g) = prj g | ||
prj _ = Nothing |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,6 @@ import Control.Carrier.Error.Either | |
import Control.Carrier.Fail.Either | ||
import Control.Carrier.Reader | ||
import Control.Carrier.State.Strict | ||
import Control.Effect.Sum | ||
import Prelude hiding (fail) | ||
import Test.Hspec | ||
import Test.Inspection as Inspection | ||
|
@@ -18,7 +17,6 @@ spec :: Spec | |
spec = do | ||
inference | ||
reinterpretation | ||
interposition | ||
fusion | ||
|
||
inference :: Spec | ||
|
@@ -62,30 +60,6 @@ instance (Carrier sig m, Effect sig) => Carrier (Reader r :+: sig) (ReinterpretR | |
eff (R other) = ReinterpretReaderC (eff (R (handleCoercible other))) | ||
|
||
|
||
interposition :: Spec | ||
interposition = describe "interposition" $ do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tbh we could have got rid of this when we added |
||
it "can interpose handlers without changing the available effects" $ | ||
run (runFail (interposeFail (fail "world"))) `shouldBe` (Left "hello, world" :: Either String Int) | ||
|
||
it "interposition only intercepts effects in its scope" $ do | ||
run (runFail (fail "world" *> interposeFail (pure (0 :: Int)))) `shouldBe` Left "world" | ||
run (runFail (interposeFail (pure (0 :: Int)) <* fail "world")) `shouldBe` Left "world" | ||
|
||
interposeFail :: InterposeC m a -> m a | ||
interposeFail = runInterposeC | ||
|
||
newtype InterposeC m a = InterposeC { runInterposeC :: m a } | ||
deriving (Applicative, Functor, Monad) | ||
|
||
instance (Carrier sig m, Member Fail sig) => MonadFail (InterposeC m) where | ||
fail s = send (Fail s) | ||
|
||
instance (Carrier sig m, Member Fail sig) => Carrier sig (InterposeC m) where | ||
eff op | ||
| Just (Fail s) <- prj op = InterposeC (send (Fail ("hello, " ++ s))) | ||
| otherwise = InterposeC (eff (handleCoercible op)) | ||
|
||
|
||
shouldSucceed :: Inspection.Result -> Expectation | ||
shouldSucceed (Success _) = pure () | ||
shouldSucceed (Failure f) = expectationFailure f | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs read a lot better with
Member
listed first, due to the extremely large number of instances involving:+:
.