lexi-lambda / freer-simple

A friendly effect system for Haskell
https://hackage.haskell.org/package/freer-simple
BSD 3-Clause "New" or "Revised" License
227 stars 19 forks source link

How to use `reinterpret2` without hardcoding effect order. #13

Closed alexjg closed 5 years ago

alexjg commented 6 years ago

I'm having an issue using reinterpret2. I have a database effect that looks something like this:

data Database r where
    GetUser :: UUID -> Database (Maybe User)

Interpreting this into IO is all pretty straightforward, but I want to interpret it into IO plus an Error effect. I guess what I want is something like the following:

runDatabase :: (Members '[IO, (Error DatabaseError)] effs') => Connection -> Eff (Database ': effs) a -> Eff effs' a

But because the return value of reinterpret2 hardcodes the efffect order I can't figure out how to do this.

Of course I might just be using reinterpret2 in an entirely inappropriate way so any advice welcome :).

lexi-lambda commented 6 years ago

The purpose of reinterpret and friends is to interpret an effect into a new effect. This is mostly because reinterpret is supposed to be used when the new effect is immediately handled as soon as it is added. A good example is the runInMemoryFileSystem handler from the documentation:

runInMemoryFileSystem :: [(FilePath, String)] -> Eff (FileSystem ': effs) ~> Eff effs
runInMemoryFileSystem initVfs = evalState initVfs . fsToState where
  fsToState :: Eff (FileSystem ': effs) ~> Eff (State [(FilePath, String)] ': effs)
  fsToState = reinterpret $ case
    ReadFile path -> get >>= \vfs -> case lookup path vfs of
      Just contents -> pure contents
      Nothing -> error ("readFile: no such file " ++ path)
    WriteFile path contents -> modify $ \vfs ->
      (path, contents) : delete (path, contents) vfs

This handler uses reinterpret to translate the FileSystem effect into a State [(FilePath, String)] effect, but it then immediately evaluates it using evalState. The introduced effect doesn’t actually appear in the type signature to runInMemoryFileSystem at all, only in the type signature for the fsToState helper function, which is an internal implementation detail.

If you intend to interpret an effect in terms of an effect that’s already in the row of effects, you shouldn’t use reinterpret. Instead, you should just use interpret, which has the following signature:

interpret :: forall eff effs. (eff ~> Eff effs) -> Eff (eff ': effs) ~> Eff effs

Since interpret allows interpreting the effect in the Eff effs monad, then you have access to any of the effects in effs inside the handler function. This means you could write your runDatabase function like this:

runDatabase :: (Member (Error DatabaseError) effs, LastMember IO effs) => Connection -> Eff (Database ': effs) ~> Eff effs
runDatabase conn = interpret $ \case
  GetUser uuid -> {- ... -}

Note that you need to use the LastMember constraint for IO so that you can use sendM or liftBase to embed IO actions (since IO isn’t a “real” effect), but you can use Member or Members as usual to specify any additional effects that must be available.

Hopefully this was helpful, and maybe the documentation can be made more effective at communicating this information than it already is.

alexjg commented 6 years ago

Ah yeah, it looks like I misunderstood the argument to interpret and didn't realise that the effs in eff ~> Eff effs could have multiple effect constraints on it. Thanks. As an aside, is there a better place to ask questions like this as I don't feel this was a bug so much as a lack of understanding on my part.

lexi-lambda commented 6 years ago

Asking questions here is okay, I think, though I might not always get to them promptly. You could also probably ask on the mailing list, IRC, the subreddit, or Stack Overflow, and other people might be able to answer those questions instead of relying on me exclusively.