Skip to content

Commit 5cf1cbb

Browse files
committed
Handle IO exceptions correctly and fix finalizers
I'm not super convinced the implementation is correct, but it passes the current tests. I'll need to dig into it more though.
1 parent 7299b62 commit 5cf1cbb

24 files changed

Lines changed: 723 additions & 387 deletions

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Theseus
22

3-
Welcome to the hub for Theseus, an effect system library for Haskell. Theseus is
4-
an effect system that supports algebraic effects (including Coroutine), many
5-
higher order effects, and is designed so that semantics do not change when
6-
effects are reordered.
3+
Welcome to the hub for Theseus, an experimental effect system library for
4+
Haskell. Theseus is an effect system that supports algebraic effects (including
5+
Coroutine), many higher order effects, and is designed so that semantics do not
6+
change when effects are reordered.
77

88
We'll explore how, much like the Ship of Theseus, programs can be deconstructed
99
and rebuilt without changing what they are.

src/Theseus/Constraints.hs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ class Anything (f :: Type -> Type)
2424

2525
instance Anything f
2626

27+
-- | A class that no types implement. It acts as a useful upper bound because
28+
-- it implies all other classes.
29+
class Nonthing (f :: Type -> Type) where
30+
produceIt :: a -> f b
31+
consumeIt :: f a -> b
32+
2733
-- | Proof that two types are the equivalent. If you construct an instance of
2834
-- this type, please make sure the isomorphism holds. For example, `Iso (const
2935
-- Nothing) (const Nothing)` would be a bad `Iso`. It's better to use the
@@ -100,6 +106,9 @@ class stronger `IsAtLeast` weaker where
100106
instance lhs `IsAtLeast` Anything where
101107
implyAtLeast = implying
102108

109+
instance Nonthing `IsAtLeast` Traversable where
110+
implyAtLeast = Implies \f -> f @[] $ Iso consumeIt produceIt
111+
103112
-- Assuming no one's doing anything bad with `Implies`, this instance should be
104113
-- equivalent to any others that apply, so we can make it incoherent.
105114
instance {-# INCOHERENT #-} same `IsAtLeast` same where

src/Theseus/Eff.hs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ module Theseus.Eff (
44
-- how Theseus works.
55

66
-- * General Effect Machinery
7-
Eff (Eff),
7+
Eff (Eff, unEff),
8+
getFacts,
9+
effUn,
10+
matchOn,
11+
Facts (Facts, bounded),
812
ControlFlow (..),
913
Anything,
14+
Nonthing,
1015
Implies (..),
1116
Iso (..),
1217
IsoSome (..),
1318
isoSomeId,
1419
isoImplying,
1520
IsAtLeast (..),
1621
implying,
22+
transImply,
1723
Effect,
1824
Freer (Pure, Impure),
1925
unrestrict,
@@ -23,7 +29,11 @@ module Theseus.Eff (
2329
Subset (raising),
2430
send,
2531
runEff,
26-
(:>),
32+
Final (Final),
33+
runEffM,
34+
(:>) (getProof),
35+
IsMember,
36+
withProof,
2737
(:>>),
2838

2939
-- * Handling Effects
@@ -43,6 +53,7 @@ module Theseus.Eff (
4353
interpretRaw,
4454
IHandlerRaw,
4555
interposeRaw,
56+
IdentityCf (IdentityCf, runIdentityCf),
4657

4758
-- * Reexports
4859
Identity,

src/Theseus/EffType.hs

Lines changed: 130 additions & 86 deletions
Large diffs are not rendered by default.

src/Theseus/Effect/Choice.hs

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ module Theseus.Effect.Choice (
1212
import Control.Applicative
1313
import Control.Monad
1414
import Control.Monad.Identity
15+
import Data.Foldable
16+
import Theseus.Constraints
1517
import Theseus.Eff
1618

1719
-- # Choice
@@ -38,20 +40,53 @@ data Many eff m a where
3840
-- need to track that ef remains compatible with our thread specific
3941
-- `Choice` interpreters. To do that we need to make sure lists are still
4042
-- valid wrappers and that all the other wrappers will be `Traversable`.
41-
[] `IsoSome` ef ->
42-
ef `Implies` Traversable ->
43+
{unMany :: [] `IsoSome` ub -> ManyItems lb ub es a} ->
44+
Many Choice (Eff lb ub es) a
45+
46+
manyUn :: IsoSome [] ub -> Many Choice (Eff lb ub es) a -> ManyItems lb ub es a
47+
manyUn = flip unMany
48+
49+
data ManyItems lb ub es a where
50+
ManyItems ::
51+
lb `Implies` Traversable ->
4352
-- These are the current values across all our threads of computation.
44-
Eff ef es [a] ->
53+
Eff lb ub es [a] ->
4554
-- This is the operation we'd like to run on each thread.
46-
(a -> Eff ef es b) ->
47-
Many Choice (Eff ef es) b
55+
(a -> Eff lb ub es b) ->
56+
ManyItems lb ub es b
57+
deriving instance Functor (ManyItems lb ub es)
4858

4959
deriving instance Functor (Many eff m)
5060

5161
instance ControlFlow Many Traversable where
52-
Many listIso travProof start go `cfApply` fa = Many listIso travProof start ((<*> fa) . go)
53-
Many listIso travProof start go `cfBind` afb = Many listIso travProof start (go >=> afb)
54-
cfMap efToOut implication handler (Many listIso _ start go) = Many (isoImplying listIso efToOut) implication (handler start) (handler . go)
62+
Many act `cfApply` fa = Many \listIso -> case act listIso of
63+
ManyItems travProof start go -> ManyItems travProof start ((<*> fa) . go)
64+
Many act `cfBind` afb = Many \listIso -> case act listIso of
65+
ManyItems travProof start go -> ManyItems travProof start (go >=> afb)
66+
cfMap ub isTrav handler (Many act) = Many \listIso -> case act (isoImplying listIso ub) of
67+
ManyItems _ start go -> ManyItems isTrav (handler start) (handler . go)
68+
cfOnce @_ @_ @eff implySend member' handler (Many act) = Many \listIso -> case act listIso of
69+
many@(ManyItems travProof _ _) -> ManyItems travProof newStart pure
70+
where
71+
implied = transImply implySend travProof
72+
newStart = do
73+
matchOn (sequenceA <$> runMany listIso member' many) >>= \case
74+
Pure as ->
75+
case handler travProof member' $ Many $ const $ ManyItems (transImply implySend travProof) as pure of
76+
Many act -> runMany listIso member' $ act listIso
77+
Impure eff lb ub lifter next -> Eff \_ -> Impure eff lb ub lifter \imply member x ->
78+
cfPutMeIn member (\starts -> runMany listIso member' $ manyUn listIso $ handler @_ @eff travProof member' $ Many \_ -> ManyItems implied starts pure) $ next imply member x
79+
cfPutMeIn member f (Many act) = Many \listIso -> case act listIso of
80+
many@(ManyItems travProof _ _) -> ManyItems travProof newStart pure
81+
where
82+
newStart = do
83+
matchOn (sequenceA <$> runMany listIso member many) >>= \case
84+
-- TODO this is probably bad because I'm flattening the threads.
85+
-- I should create some tests to either show why this is fine or show
86+
-- why it's bad.
87+
Pure as -> fmap pure $ f $ fmap fold as
88+
Impure eff lb ub lifter next -> Eff \_ -> Impure eff lb ub lifter \imply member x ->
89+
fmap pure $ cfPutMeIn member f $ fmap fold <$> next imply member x
5590

5691
-- Most `Choice` implementations are depth first. They run one thread to
5792
-- completion, then they run the next. The problem with depth first is that
@@ -62,11 +97,12 @@ instance ControlFlow Many Traversable where
6297
-- parallel. That means handlers aren't constantly going in and out of scope
6398
-- because all the threads will join before the interpreter goes out of
6499
-- scope.
65-
cfRun imply@(Implies go) handler many@(Many listIso _ _ _) = Many listIso imply newStart pure
66-
where
67-
-- We require `Traversable` because we need to know how many threads made
68-
-- it past the other interpreter.
69-
newStart = go \(Iso lr rl) -> fmap (fmap rl . sequenceA . lr) $ handler $ runMany many
100+
cfRun member handler (Many act) = Many \listIso -> case act listIso of
101+
many@(ManyItems imply@(Implies go) _ _) -> ManyItems imply newStart pure
102+
where
103+
-- We require `Traversable` because we need to know how many threads made
104+
-- it past the other interpreter.
105+
newStart = go \(Iso lr rl) -> fmap (fmap rl . sequenceA . lr) $ handler $ runMany listIso member many
70106

71107
-- There we go! That's how Theseus handles nondeterminism while avoiding the
72108
-- handler ordering problems.
@@ -76,57 +112,64 @@ instance ControlFlow Many Traversable where
76112
data Choice :: Effect where
77113
-- Do these constraints leak unfortunate implementation details? Yes. I don't
78114
-- know how to get rid of them though.
79-
Empty :: (ef [], ef `IsAtLeast` Traversable) => Choice (Eff ef es) a
80-
Choose :: (ef [], ef `IsAtLeast` Traversable) => Choice (Eff ef es) Bool
115+
Empty :: Choice (Eff lb ub es) a
116+
Choose :: Choice (Eff lb ub es) Bool
81117

82118
-- | Runs a choice that causes all other effects to act globally over all the
83119
-- threads.
84120
runChoice ::
85-
(ef `IsAtLeast` Traversable, ef []) =>
86-
Eff ef (Choice : es) a ->
87-
Eff ef es [a]
88-
runChoice = interpretRaw (pure . pure) \cases
89-
Empty _ next ->
90-
case next $ Many isoSomeId implyAtLeast (pure []) pure of
91-
Many listIso travProof start go ->
121+
(lb `IsAtLeast` Traversable, ub []) =>
122+
Eff lb ub (Choice : es) a ->
123+
Eff lb ub es [a]
124+
runChoice act = Eff \facts@Facts{bounded} -> effUn facts $ with act $ interpretRaw (isoImplying isoSomeId bounded) (pure . pure) \cases
125+
Empty lb _ _ next ->
126+
case manyUn isoSomeId $ next implyAtLeast (Just getProof) $ Many \_ -> ManyItems (transImply lb implyAtLeast) (pure []) pure of
127+
ManyItems travProof start go ->
92128
join <$> runChoice do
93129
inits <- start
94-
results <- traverse (poseChoice listIso travProof . go) inits
130+
results <- traverse (poseChoice isoSomeId travProof . go) inits
95131
pure $ join results
96-
Choose _ next ->
97-
case next $ Many isoSomeId implyAtLeast (pure [True, False]) pure of
98-
Many listIso travProof start go ->
132+
Choose lb _ _ next ->
133+
case manyUn isoSomeId $ next implyAtLeast (Just getProof) $ Many \_ -> ManyItems (transImply lb implyAtLeast) (pure [True, False]) pure of
134+
ManyItems travProof start go ->
99135
join <$> runChoice do
100136
inits <- start
101-
results <- traverse (poseChoice listIso travProof . go) inits
137+
results <- traverse (poseChoice isoSomeId travProof . go) inits
102138
pure $ join results
103139

104140
-- | Same as `runChoice`, but modifies a `Choice` that's not at the top of the
105141
-- stack.
106142
poseChoice ::
107143
Choice :> es =>
108-
[] `IsoSome` ef ->
109-
ef `Implies` Traversable ->
110-
Eff ef es a ->
111-
Eff ef es [a]
112-
poseChoice (IsoSome (Iso lg gl)) imply =
113-
fmap gl . interposeRaw imply (lg . pure) \cases
114-
Empty _ next ->
115-
case next $ Many isoSomeId implyAtLeast (pure []) pure of
116-
many -> lg <$> runMany many
117-
Choose _ next ->
118-
case next $ Many isoSomeId implyAtLeast (pure [True, False]) pure of
119-
many -> lg <$> runMany many
144+
[] `IsoSome` ub ->
145+
lb `Implies` Traversable ->
146+
Eff lb ub es a ->
147+
Eff lb ub es [a]
148+
poseChoice isoUb imply action = do
149+
Facts{bounded} <- getFacts
150+
IsoSome (Iso lg gl) <- pure $ isoImplying isoUb bounded
151+
fmap gl $ with action $ interposeRaw imply (lg . pure) \cases
152+
Empty lb _ _ next ->
153+
case manyUn isoUb $ next $ Many \_ -> ManyItems (transImply lb imply) (pure []) pure of
154+
many -> lg <$> runMany isoUb (Just getProof) many
155+
Choose lb _ _ next ->
156+
case manyUn isoUb $ next $ Many \_ -> ManyItems (transImply lb imply) (pure [True, False]) pure of
157+
many -> lg <$> runMany isoUb (Just getProof) many
120158

121159
-- | Executes all the threads
122-
runMany :: Choice :> es => Many Choice (Eff ef es) a -> Eff ef es [a]
123-
runMany (Many isoSomeId travProof start go) =
124-
join <$> poseChoice isoSomeId travProof do
160+
runMany :: [] `IsoSome` ub -> Maybe (Choice `IsMember` es) -> ManyItems lb ub es a -> Eff lb ub es [a]
161+
runMany listIso (Just proof) (ManyItems travProof start go) = withProof proof do
162+
join <$> poseChoice listIso travProof do
125163
inits <- start
126-
results <- traverse (poseChoice isoSomeId travProof . go) inits
164+
results <- traverse (poseChoice listIso travProof . go) inits
127165
pure $ join results
166+
-- In this case we know that none of the computations will use `Choice`, so we
167+
-- don't need to distribute.
168+
runMany _ Nothing (ManyItems _ start go) = do
169+
inits <- start
170+
traverse go inits
128171

129-
instance (Choice :> es, ef [], ef `IsAtLeast` Traversable) => Alternative (Eff ef es) where
172+
instance (Choice :> es, lb [], lb `IsAtLeast` Traversable) => Alternative (Eff lb ub es) where
130173
empty = send Empty
131174
a <|> b =
132175
send Choose >>= \case
@@ -135,12 +178,12 @@ instance (Choice :> es, ef [], ef `IsAtLeast` Traversable) => Alternative (Eff e
135178

136179
-- | A provider for Choices. You can use this to scope the `Choice` threads.
137180
data Collect :: Effect where
138-
Collect :: (ef `IsAtLeast` Traversable, ef []) => Eff ef (Choice : es) a -> Collect (Eff ef es) [a]
181+
Collect :: (lb `IsAtLeast` Traversable, ub []) => Eff lb ub (Choice : es) a -> Collect (Eff lb ub es) [a]
139182

140183
-- | Gathers all the threads of computation into a list.
141-
collect :: (Collect :> es, ef []) => ef `IsAtLeast` Traversable => Eff ef (Choice : es) a -> Eff ef es [a]
184+
collect :: (Collect :> es, ub [], lb `IsAtLeast` Traversable) => Eff lb ub (Choice : es) a -> Eff lb ub es [a]
142185
collect action = send $ Collect action
143186

144187
-- | Provides the default `Choice` implementation.
145-
runCollect :: ef Identity => Eff ef (Collect : es) a -> Eff ef es a
188+
runCollect :: lb Identity => Eff lb ub (Collect : es) a -> Eff lb ub es a
146189
runCollect = interpret \_ (Collect action) -> pure $ runChoice action

src/Theseus/Effect/Coroutine.hs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ data Coroutine a b m c where
1515
Yield :: a -> Coroutine a b m b
1616

1717
-- | Pauses the computation providing `a` value and waiting on `b`.
18-
yield :: forall a b ef es. Coroutine a b :> es => a -> Eff ef es b
18+
yield :: forall a b lb ub es. Coroutine a b :> es => a -> Eff lb ub es b
1919
yield a = send $ Yield a
2020

2121
-- | Runs a computation until it either completes or pauses. If it pauses, it
2222
-- can be resumed.
23-
runCoroutine :: forall a b c ef es. ef (Status ef es a b) => Eff ef (Coroutine a b : es) c -> Eff ef es (Status ef es a b c)
24-
runCoroutine = interpretRaw (pure . Done) \(Yield a) _ next ->
25-
case next $ Yielding pure of
23+
runCoroutine :: forall a b c lb ub es. lb (Status lb ub es a b) => Eff lb ub (Coroutine a b : es) c -> Eff lb ub es (Status lb ub es a b c)
24+
runCoroutine = interpretRaw isoSomeId (pure . Done) \(Yield a) _ _ _ next ->
25+
case next implying (Just $ getProof @(Coroutine a b)) $ Yielding pure of
2626
Yielding yielding -> pure $ Yielded a yielding
2727

2828
-- | It is essential that the function provided by `Yielded` be used exactly
2929
-- once. Otherwise you'll get confusing semantics within your code.
30-
data Status ef es a b c = Done c | Yielded a (b -> Eff ef (Coroutine a b : es) c)
30+
data Status lb ub es a b c = Done c | Yielded a (b -> Eff lb ub (Coroutine a b : es) c)
3131
deriving (Functor)
3232

3333
newtype Yielding b eff m c = Yielding {yielded :: b -> m c}
@@ -36,5 +36,15 @@ newtype Yielding b eff m c = Yielding {yielded :: b -> m c}
3636
instance ControlFlow (Yielding b) Anything where
3737
Yielding bmc `cfApply` fa = Yielding \b -> bmc b <*> fa
3838
Yielding bmc `cfBind` afb = Yielding $ bmc >=> afb
39+
cfOnce ogLb member handler (Yielding @_ @eff go) = Yielding \b -> do
40+
matchOn (go b) >>= \case
41+
Pure a -> runIdentityCf $ handler implying member $ IdentityCf @eff a
42+
Impure eff lb ub lifter next -> Eff \_ -> Impure eff lb ub lifter \imply member x ->
43+
cfOnce ogLb member handler $ next imply member x
44+
cfPutMeIn _ f (Yielding go) = Yielding \b -> do
45+
matchOn (go b) >>= \case
46+
Pure a -> f a
47+
Impure eff lb ub lifter next -> Eff \_ -> Impure eff lb ub lifter \imply member x ->
48+
cfPutMeIn member f $ next imply member x
3949
cfMap _ _ handler (Yielding bmc) = Yielding $ handler . bmc
4050
cfRun _ handler (Yielding bmc) = Yielding $ handler . bmc

0 commit comments

Comments
 (0)