Skip to content
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

Difference to effectful #2

Open
maralorn opened this issue Apr 1, 2024 · 21 comments
Open

Difference to effectful #2

maralorn opened this issue Apr 1, 2024 · 21 comments

Comments

@maralorn
Copy link
Contributor

maralorn commented Apr 1, 2024

Hey there!

I am really intrigued by bluefin, it looks very cool. I am however a bit confused but the design and was hoping to for some clarifying discussion.

The handler as function arguments pattern is indeed very expressive and simple. However it immediately triggers a: So many function parameters, maybe this would be easier with a Reader? Which kinda defeats the purpose. I have a few questions about that:

  • passing handles around as values feels runtime heavy, can that be a performance problem?
  • Since the handlers should be known at compile time could we make the the handler parameters type level arguments? e.g. with visible forall in 9.10?
  • once we have that, can't we try to infer the type arguments by making them implicit? Worst case that doesn't help because GHC can never infer the type, but maybe it can?
  • Once we are there, isn't that quite similar to effectful? But bluefin seems to be more potent in the kind of effects which can be implemented. Is that really because of the termlevel handlers or is there something else at play?

Thank you for indulging me.

@tomjaguarpaw
Copy link
Owner

Hi @maralorn, thank you for your interest in Bluefin and your good questions! I suspect similar questions are going to come up a lot, so it's helpful for me to practice my answers.

So many function parameters

I think in practice there won't be so many function parameters. Users will bundle parameters that are commonly used together into product types (just like we do with normal function parameters).

maybe this would be easier with a Reader? Which kinda defeats the purpose

I'm not sure it defeats the purpose. I think the ideal situation would be to have the explicit Bluefin style and the implicit effectful style available in the same library, if the cost of switching between the styles can be kept low. I think it's plausible that an effectful style API could be layered on Bluefin. I think it's less likely that Bluefin style API could be layered on top of effectful (because it would be hard to convince effectful to have two effects of the same type in the same operation) but perhaps I've missed something.

passing handles around as values feels runtime heavy, can that be a performance problem?

I haven't benchmarked, but I doubt there's a performance problem passing handles around. Passing arguments is one of the cheapest things you can do, requiring no allocation. It's also the kind of thing that's very easy to eliminate with inlining. I certainly don't see how it can be more expensive than the closest alternative: passing arguments implicitly in a class constraint.

Since the handlers should be known at compile time could we make the the handler parameters type level arguments? e.g. with visible forall in 9.10?

I don't understand the connection between knowing the handlers at compile time and making them type level arguments. Type level arguments need not be known at compile time!

In any case, I have tried something similar but it seemed less ergonomic to me. You need TypeAbstractions in function arguments to make it remotely tractable, since you need to bind the handle in the argument to each handler, and we don't have those yet (I'm not sure about 9.10). Then you need to provide the argument as an explicit type application at each call site. (You may have been hoping to avoid that!)

There is an another alternative approach to implement an effectful style API on top of Bluefin, but I haven't fully worked it out yet.

once we have that, can't we try to infer the type arguments by making them implicit? Worst case that doesn't help because GHC can never infer the type, but maybe it can?

Yes, I guess you could do that. Then indeed GHC can infer the types (but, to reiterate, you need to apply the type applications explicitly at each call site, so it probably doesn't help you do what you want.)

The alternative approach could infer the types if they are unique ...

Once we are there, isn't that quite similar to effectful? But bluefin seems to be more potent in the kind of effects which can be implemented. Is that really because of the termlevel handlers or is there something else at play?

The alternative approach would be quite similar to effectful. In fact it would be so similar that it would be Bluefin redundant, unless you want to mix-and-match Bluefin and effectful styles, as I described above.

I don't think Bluefin is more "potent" in terms of effects that can be implemented. Despite saying in Bluefin's introduction that I don't know how to support Coroutine in effectful, @Lysxia showed me how. I suspect something like Compound is possible too.

The big difference is that Bluefin makes effect disambiguation a non-issue. Already, having to disambiguate effects by type is a big ergonomic drag (effectful's author suggests newtypes when you want to use two states of the same type, for example) but there seems to be something about coroutines that requires type annotations even when there should be no ambiguity (I think it might be the fact that the effect has two type parameters).

Because Bluefin has no difficulties with disambiguation (the "cost has already been paid" by making handles explicit) it means it can be extremely flexible in other ways, for example Stream a is a synonym for Coroutine a (), EarlyReturn is a synonym for Exception and Jump is a synonym for EarlyReturn (). That kind of flexibility is hard to achieve with any system that uses types for disambiguation. Each of those effects would have to be a newtype (and with Jump it's not even clear what you would newtype: it doesn't have a type parameter!). That's very heavyweight.

To conclude this section I'll point out that I'm not convinced that explicit effect handles are actually a cost, in ergonomic terms. After using them for a while I actually prefer them to implicitly passed effects.

Thank you for indulging me.

You're welcome. I'm happy to help. Please do continue to ask any questions you may have or share any comments.

@maralorn
Copy link
Contributor Author

maralorn commented Apr 1, 2024

I guess I’ll just try it out and see how it goes.

@tomjaguarpaw
Copy link
Owner

Great, please report your feedback!

@Lysxia
Copy link
Contributor

Lysxia commented Apr 1, 2024

I've been thinking about this area for a while too so thanks for starting this discussion!

passing handles around as values feels runtime heavy, can that be a performance problem?

Under the hood, effectful also passes handles around, since effectful's monad is a ReaderT Handle IO. The handles are being passed as an array, and adding a handler on the stack copies the array.

In bluefin, there is no implicit array/record of handlers. All of the handler passing is explicit.

A simple illustrative situation is two handlers on top of a call to the outer handler.

-- effectful: Eff es a = Env es -> IO a
handler1 :: Eff (E1 : es) B -> Eff es C
handler2 :: Eff (E2 : es) A -> Eff es B
call :: E1 :> es => Eff es A

example :: Eff es C
example = handler1 (handler2 call)
-- bluefin: Eff ss a = IO a
handler1 :: (E1 s -> Eff (s : ss) B) -> Eff ss C
handler2 :: (E2 s -> Eff (s : ss) A) -> Eff ss B
call :: s :> ss => E1 s -> Eff ss A

example :: Eff es C
example = handler1 (\h1 -> handler2 (\_ -> call h1))
  • In effectful, the Env argument of call is supplied by handler2 which appends its own handle to the Env received from handler1.
  • In bluefin, call is directly applied to the handle from handler1. And if h1 is a primitive resource like IORef for the State effect, the compiler can easily simplify this code to what you'd write using the underlying primitives.

A remark on ergonomics: if the effects E1 and E2 are identical, then the effectful example above changes semantics: call will access the inner handler. Maybe the average user of effectful only ever uses application-specific effects so they're always distinct. Personally, I ran straight into a situation with duplicate effects on the stack when I adapted bluefin's example code with coroutine.

For some related literature, Bluefin is similar to the named handlers of the Koka language, a feature presented in the paper "First-class names for effect handlers". Koka supports both unnamed handlers and named handlers, and uses the same rank-2 type trick as bluefin to keep track of the scope of names/handles.

Named handlers are a form of capability, and there's work exploring that connection implemented in the Effekt language.

@maralorn
Copy link
Contributor Author

maralorn commented Apr 2, 2024

Okay, I will just continue abusing this thread for my questions. Please feel free to redirect me, e.g. to the Haskell discourse or anywhere else.

  1. I fear a bit off more than I can chew, by trying to convert an mtl style effect. The provided handler has the signature:

    runMyMonad :: forall r. (forall m. (MyMonad m, MonadIO m) => m r) -> IO r
    

    I then would like an effect with a method something like

    runInMyMonad :: (e :> es) => MyMonadHandler e -> (forall m. MyMonad m => m r) -> Eff es r
    

    At first I thought I could do this in effectful (because what I am trying is similar to the Handler example in the effectful docs. But I think that example is to specific. Do you know of a way I can do this (in effectful or bluefin)? (Full disclosure the real world runner I am trying to implement is reflex).

  2. While pondering the different ways bluefin implements handlers I noticed that I can’t find something akin to the dynamic dispatch concept with different runtime choosable interpretations like in effectful. All the handlers I looked at seem quite "Static". Do you think bluefin can also support that usecase? (which is often brought as a selling point for testing, etc.).

@Lysxia
Copy link
Contributor

Lysxia commented Apr 3, 2024

For adapting an existing mtl-style effect, have a look at the Dispatch modules (static or dynamic): https://hackage.haskell.org/package/effectful-core-2.3.0.1/docs/Effectful-Dispatch-Dynamic.html#g:4
(Edited out: code that was wrong. You need something more involved like what tom suggests in this comment below #2 (comment))


I believe that "dynamic effects" as they are called in effectful are those where the interface of the effect is encoded as a (G)ADT. For example, the State effect in the bluefin library is "static" because get and put are already specialized to readIORef and writeIORef, and there is no way to change that interpretation after the fact. For a dynamic state effect, you define the following GADT:

data StateOp s a where
  Get :: StateOp s s
  Put :: s -> StateOp s ()

Then the type of "handle"/"environment" that is being passed around can be exactly the handlers of those operations

data State s e = UnsafeState (forall a. StateOp s a -> IO a)  -- Instead of Bluefin.State.State

You now have all the freedom to give new interpretations to Get and Put.

An additional "dynamic" ability that you get in effectful is to override an existing handler using impose. This is possible because handlers can mess with all handles of existing handlers on the stack. In bluefin, you could have a primitive to create new handles for effects that are already on the stack:

newStateHandle :: e :> es => (forall a. StateOp s a -> Eff es a) -> (State s e -> Eff es a) -> Eff es a

then the equivalent of impose is to modify the handled computation to use that new handle instead of the old one.

@maralorn
Copy link
Contributor Author

maralorn commented Apr 3, 2024

Thank you @Lysxia for your extensive example. That makes a lot of sense to me.

Sadly I underspecified the problem I am trying to solve a bit. In your example runMyMonad gets invoked newly everytime call is used. But the real runMyMonad I want to deal with has some kind of initialisation and state between calls and thus I need to use the same runMyMonad call for all call uses. The longer I think about it the more I believe that is only possible if MyMonad also implements MonadUnliftIO

@tomjaguarpaw
Copy link
Owner

I will just continue abusing this thread for my questions.

It's fine by me to continue here.

I fear a bit off more than I can chew, by trying to convert an mtl style effect. The provided handler has the signature:

runMyMonad :: forall r. (forall m. (MyMonad m, MonadIO m) => m r) -> IO r

I don't think I understand clearly enough what you're asking. I understand that you are using a library that has a function of this type (and the reflex example seems to confirm that). Are you asking how to run a Bluefin program with this handler? I guess not, because the higher-order argument to runMyMonad must be fully polymorphic in its monad. Are you asking how you would implement runMyMonad in a "Bluefin way", to see the alternative approach?

@maralorn
Copy link
Contributor Author

maralorn commented Apr 3, 2024

I was hoping to use runMyMonad to define and run an effect with bluefin. But I assume that won’t be possible without introspecting what MyMonad really does and possibly replacing/reimplementing it.

In the end I would take any solution which would make reflex compatible with bluefin or effectful. The effectful documentation made me hope that this might be possible (because it gives so many examples of being compatible with a lot of stuff) but I currently don’t see how this is achievable.

@tomjaguarpaw
Copy link
Owner

I was hoping to use runMyMonad to define and run an effect with bluefin.

You won't be able to use runMyMonad for anything to do with Bluefin or effectful. Its type, (forall m. (MyMonad m, MonadIO m) => m r) -> IO r, implies that the only thing you can use when you define your m r is that it is MyMonad and MonadIO. You can't even use MonadState m, say, let alone m ~ Eff es.

On the other hand it's likely there are either primitives or internal Reflex functions that would allow you to do what you want. It looks like runHeadlessApp instantiates m to a composition of things like TriggerEventT, PostBuildT, ... . If you can expose a function that runs that concrete stack, and if all the monads in the stack are basically ReaderT r IO, then you'll be able to write a Bluefin- (or effectful-) compatible interface.

@maralorn
Copy link
Contributor Author

maralorn commented Apr 3, 2024

Yeah, I realize that I will really need to dig into that stack to figure out if I can mirror it.

Another thought which came to mind: Have you thought about using Bluefin with ImplicitParameters for the handles? I haven’t used it and it is apparently not a very popular extension but it might be particularly useful for this?

@tomjaguarpaw
Copy link
Owner

Have you thought about using Bluefin with ImplicitParameters for the handles?

Yeah, it doesn't seem to work terribly well:

d55a7c9

@maralorn
Copy link
Contributor Author

maralorn commented Apr 3, 2024 via email

@maralorn
Copy link
Contributor Author

maralorn commented Apr 3, 2024

Just fyi, I tried to play around with this a bit and I figured out how to define and run a Reflex effect. Its just the first step of the rather intricate transformer stack used in reflex, but with the POC working I think it will be possible.

You can find my example here: https://code.maralorn.de/maralorn/config/src/commit/646fb8833c181fd16a3c486b8a876c85f861f654/packages/kass/lib/Bluefin/Reflex.hs

@tomjaguarpaw
Copy link
Owner

Thanks to your prompting I wrote up an example of how to do dynamic effects with Blufin. It's really simple! (Although it could do with some ergonomics massaging.) You basically just create a record of operations, and then define them by delegating to other handlers.

data Filesystem es = MkFilesystem
{ readFile :: FilePath -> Eff es String,
writeFile :: FilePath -> String -> Eff es ()
}
action :: (e :> es) => Filesystem e -> Eff es Bool
action fs = do
file <- weakenEff has (readFile fs "/dev/null")
pure (length file > 0)
runFileSystemPure ::
(e1 :> es) =>
Exception String e1 ->
[(FilePath, String)] ->
(forall e2. Filesystem e2 -> Eff (e2 :& es) r) ->
Eff es r
runFileSystemPure ex fs0 k =
mergeEff $
evalState fs0 $ \fs ->
assoc1Eff $
k
MkFilesystem
{ readFile = \path -> do
fs' <- get fs
case lookup path fs' of
Nothing -> throw ex ("File not found: " <> path)
Just s -> pure s,
writeFile = \path contents ->
modify fs ((path, contents) :)
}
runFileSystemIO ::
(e1 :> es, e2 :> es) =>
Exception String e1 ->
IOE e2 ->
(forall e. Filesystem e -> Eff (e :& es) r) ->
Eff es r
runFileSystemIO ex io k =
mergeEff $
k
MkFilesystem
{ readFile =
adapt . Prelude.readFile,
writeFile =
\path -> adapt . Prelude.writeFile path
}
where
adapt m =
effIO io (Control.Exception.try @IOException m) >>= \case
Left e -> throw ex (show e)
Right r -> pure r
exampleRunFileSystemPure :: Either String Bool
exampleRunFileSystemPure = runPureEff $ try $ \ex ->
runFileSystemPure ex [("/dev/null", "")] action
exampleRunFileSystemIO :: IO (Either String Bool)
exampleRunFileSystemIO = runEff $ \io -> try $ \ex ->
runFileSystemIO ex io action

@maralorn
Copy link
Contributor Author

maralorn commented Apr 4, 2024 via email

@tomjaguarpaw
Copy link
Owner

Just the weakenEff has feels a bit unergonomic. ... Generally at some point in the future a guide to the different effects manipulation operators like weakenEff, mergeEff would be awesome. But maybe best to wait a bit to discover which patterns are most common and useful.

Yeah exactly, it is unergonomic, but I'm sure in time we'll work out an ergonomic way of doing it.

Also I realize how "methody" or just "namespacy" this can feel if you do fs.readFile. That’s pretty neat.

Ah yes! It looks very nice with OverloadedRecordDot. I didn't think of that.

@tomjaguarpaw
Copy link
Owner

This is a bit nicer now. You only need inContext and useImpl.

data Filesystem es = MkFilesystem
{ readFileImpl :: FilePath -> Eff es String,
writeFileImpl :: FilePath -> String -> Eff es ()
}
readFile :: (e :> es) => Filesystem e -> FilePath -> Eff es String
readFile fs filepath = useImpl (readFileImpl fs filepath)
writeFile :: (e :> es) => Filesystem e -> FilePath -> Eff es String
writeFile fs filepath = useImpl (readFileImpl fs filepath)
action :: (e :> es) => Filesystem e -> Eff es Bool
action fs = do
file <- readFile fs "/dev/null"
pure (length file > 0)
runFileSystemPure ::
(e1 :> es) =>
Exception String e1 ->
[(FilePath, String)] ->
(forall e2. Filesystem e2 -> Eff (e2 :& es) r) ->
Eff es r
runFileSystemPure ex fs0 k =
evalState () $ \_ ->
evalState fs0 $ \fs ->
inContext $
k
MkFilesystem
{ readFileImpl = \path -> do
fs' <- get fs
case lookup path fs' of
Nothing -> throw ex ("File not found: " <> path)
Just s -> pure s,
writeFileImpl = \path contents ->
modify fs ((path, contents) :)
}
runFileSystemIO ::
forall e1 e2 es r.
(e1 :> es, e2 :> es) =>
Exception String e1 ->
IOE e2 ->
(forall e. Filesystem e -> Eff (e :& es) r) ->
Eff es r
runFileSystemIO ex io k =
evalState () $ \_ ->
inContext $
k
MkFilesystem
{ readFileImpl =
adapt . Prelude.readFile,
writeFileImpl =
\path -> adapt . Prelude.writeFile path
}
where
adapt :: (e1 :> ess, e2 :> ess) => IO a -> Eff ess a
adapt m =
effIO io (Control.Exception.try @IOException m) >>= \case
Left e -> throw ex (show e)
Right r -> pure r
exampleRunFileSystemPure :: Either String Bool
exampleRunFileSystemPure = runPureEff $ try $ \ex ->
runFileSystemPure ex [("/dev/null", "")] action
exampleRunFileSystemIO :: IO (Either String Bool)
exampleRunFileSystemIO = runEff $ \io -> try $ \ex ->
runFileSystemIO ex io action

@maralorn
Copy link
Contributor Author

maralorn commented Apr 5, 2024

Nice, although the evalState () is confusing me a bit.

I do have by-the-way now a full blown working reflex effect going and I think I can pull of the same for reflex-dom. I am very pleased. (Progress here: https://code.maralorn.de/maralorn/config/src/branch/main/packages/kass/lib/Bluefin/Reflex.hs)

@tomjaguarpaw
Copy link
Owner

Nice, although the evalState () is confusing me a bit.

Oh, that's just a dummy handler to prove to myself that what I'm doing works in general, not just when there's only one handler.

I do have by-the-way now a full blown working reflex effect

Great!

@tomjaguarpaw
Copy link
Owner

I've got something I'm content with now. Here's a progressive worked example of a "counter" effect, followed by the filesystem effect. I'll write this up as a page in the Haddocks.

-- Counter 1
newtype Counter1 e = MkCounter1 (State Int e)
incCounter1 :: (e :> es) => Counter1 e -> Eff es ()
incCounter1 (MkCounter1 st) = modify st (+ 1)
runCounter1 ::
(forall e. Counter1 e -> Eff (e :& es) r) ->
Eff es Int
runCounter1 k =
evalState 0 $ \st -> do
_ <- useImplIn k (MkCounter1 (mapHandle st))
get st
-- Counter 2
data Counter2 e = MkCounter2 (State Int e) (Exception () e)
incCounter2 :: (e :> es) => Counter2 e -> Eff es ()
incCounter2 (MkCounter2 st ex) = do
count <- get st
when (count >= 10) $
throw ex ()
put st (count + 1)
runCounter2 ::
(forall e. Counter2 e -> Eff (e :& es) r) ->
Eff es Int
runCounter2 k =
evalState 0 $ \st -> do
_ <- try $ \ex -> do
useImplIn k (MkCounter2 (mapHandle st) (mapHandle ex))
get st
-- Counter 3
data Counter3 e
= MkCounter3 (State Int e) (Exception () e) (Stream String e)
incCounter3 :: (e :> es) => Counter3 e -> Eff es ()
incCounter3 (MkCounter3 st ex y) = do
count <- get st
when (even count) $
yield y "Count was even"
when (count >= 10) $
throw ex ()
put st (count + 1)
getCounter3 :: (e :> es) => Counter3 e -> String -> Eff es Int
getCounter3 (MkCounter3 st _ y) msg = do
yield y msg
get st
runCounter3 ::
(e1 :> es) =>
Stream String e1 ->
(forall e. Counter3 e -> Eff (e :& es) r) ->
Eff es Int
runCounter3 y k =
evalState 0 $ \st -> do
_ <- try $ \ex -> do
useImplIn k (MkCounter3 (mapHandle st) (mapHandle ex) (mapHandle y))
get st
-- Counter 4
data Counter4 e = MkCounter4
{ incCounter4Impl :: Eff e (),
getCounter4Impl :: String -> Eff e Int
}
incCounter4 :: (e :> es) => Counter4 e -> Eff es ()
incCounter4 e = useImpl (incCounter4Impl e)
getCounter4 :: (e :> es) => Counter4 e -> String -> Eff es Int
getCounter4 e msg = useImpl (getCounter4Impl e msg)
runCounter4 ::
(e1 :> es) =>
Stream String e1 ->
(forall e. Counter4 e -> Eff (e :& es) r) ->
Eff es Int
runCounter4 y k =
evalState 0 $ \st -> do
_ <- try $ \ex -> do
useImplIn
k
( MkCounter4
{ incCounter4Impl = do
count <- get st
when (even count) $
yield y "Count was even"
when (count >= 10) $
throw ex ()
put st (count + 1),
getCounter4Impl = \msg -> do
yield y msg
get st
}
)
get st
-- Counter 5
data Counter5 e = MkCounter5
{ incCounter5Impl :: Eff e (),
counter5State :: State Int e,
counter5Stream :: Stream String e
}
incCounter5 :: (e :> es) => Counter5 e -> Eff es ()
incCounter5 e = useImpl (incCounter5Impl e)
getCounter5 :: (e :> es) => Counter5 e -> String -> Eff es Int
getCounter5 (MkCounter5 _ st y) msg = do
yield y msg
get st
runCounter5 ::
(e1 :> es) =>
Stream String e1 ->
(forall e. Counter5 e -> Eff (e :& es) r) ->
Eff es Int
runCounter5 y k =
evalState 0 $ \st -> do
_ <- try $ \ex -> do
useImplIn
k
( MkCounter5
{ incCounter5Impl = do
count <- get st
when (even count) $
yield y "Count was even"
when (count >= 10) $
throw ex ()
put st (count + 1),
counter5State = mapHandle st,
counter5Stream = mapHandle y
}
)
get st
-- Filesystem
data Filesystem es = MkFilesystem
{ readFileImpl :: FilePath -> Eff es String,
writeFileImpl :: FilePath -> String -> Eff es ()
}
readFile :: (e :> es) => Filesystem e -> FilePath -> Eff es String
readFile fs filepath = useImpl (readFileImpl fs filepath)
writeFile :: (e :> es) => Filesystem e -> FilePath -> Eff es String
writeFile fs filepath = useImpl (readFileImpl fs filepath)
action :: (e :> es) => Filesystem e -> Eff es Bool
action fs = do
file <- readFile fs "/dev/null"
pure (length file > 0)
runFileSystemPure ::
(e1 :> es, e3 :> es) =>
Exception String e1 ->
Exception String e3 ->
[(FilePath, String)] ->
(forall e2. Filesystem e2 -> Eff (e2 :& es) r) ->
Eff es r
runFileSystemPure ex ex2 fs0 k =
evalState fs0 $ \fs ->
useImplIn
k
MkFilesystem
{ readFileImpl = \path -> do
fs' <- get fs
case lookup path fs' of
Nothing ->
throw ex ("File not found: " <> path)
>> throw ex2 ""
Just s -> pure s,
writeFileImpl = \path contents ->
modify fs ((path, contents) :)
}
runFileSystemIO ::
forall e1 e2 es r.
(e1 :> es, e2 :> es) =>
Exception String e1 ->
IOE e2 ->
(forall e. Filesystem e -> Eff (e :& es) r) ->
Eff es r
runFileSystemIO ex io k =
useImplIn
k
MkFilesystem
{ readFileImpl =
adapt . Prelude.readFile,
writeFileImpl =
\path -> adapt . Prelude.writeFile path
}
where
adapt :: (e1 :> ess, e2 :> ess) => IO a -> Eff ess a
adapt m =
effIO io (Control.Exception.try @IOException m) >>= \case
Left e -> throw ex (show e)
Right r -> pure r
exampleRunFileSystemPure :: Either String Bool
exampleRunFileSystemPure = runPureEff $ try $ \ex ->
runFileSystemPure ex ex [("/dev/null", "")] action
exampleRunFileSystemIO :: IO (Either String Bool)
exampleRunFileSystemIO = runEff $ \io -> try $ \ex ->
runFileSystemIO ex io action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants