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

How to have dynamic effects that take handles as arguments #17

Closed
oberblastmeister opened this issue Jun 14, 2024 · 7 comments
Closed

Comments

@oberblastmeister
Copy link

As an example, suppose that we have a file system effect. We might want to be able to handle errors in the readFile function, but still have it be dynamically interpreted while using some other unknown effects. What we want is something similar to the incorrect code below.

data Filesystem e = Filesystem {
   readFile :: Exception FileSystemError e -> FilePath -> Eff e String,
   ...
}
@tomjaguarpaw
Copy link
Owner

This is an interesting question! I had not thought of doing something like this. Looks like it is possible using the strategy in 15efc71. I wonder if there's a convenient way of incorporating this into @Lysxia's recipe for compound effects at #14.

@oberblastmeister
Copy link
Author

Here's how I did it:

data FsWeird es = MkFsWeird
  { readFileImpl :: forall ex sm. Exception String ex -> Stream Int sm -> FilePath -> Eff (sm :& ex :& es) String,
    writeFileImpl :: FilePath -> String -> Eff es ()
  }

instance Handle FsWeird where
  mapHandle fs@MkFsWeird {readFileImpl, writeFileImpl} =
    MkFsWeird
      { readFileImpl = \ex sm fp -> useImpl2 $ readFileImpl ex sm fp,
        writeFileImpl = (fmap . fmap) useImpl writeFileImpl
      }

readFile :: (e :> es, ex :> es, sm :> es) => Exception String ex -> Stream Int sm -> FsWeird e -> FilePath -> Eff es String
readFile ex sm MkFsWeird {readFileImpl} fp = subsume2L $ useImpl2 $ readFileImpl ex sm fp

writeFile :: (e :> es) => FileSystem e -> FilePath -> String -> Eff es ()
writeFile fs filepath contents = useImpl (fs.writeFileImpl filepath contents)

runFileSystemPure ::
  (e0 :> es) =>
  State Int e0 ->
  [(FilePath, String)] ->
  (forall e2. FsWeird e2 -> Eff (e2 :& es) r) ->
  Eff es r
runFileSystemPure st fs0 k =
  evalState fs0 $ \fs ->
    useImplIn
      k
      MkFsWeird
        { readFileImpl = \ex sm filepath -> do
            fs' <- get fs
            yield sm 1
            yield sm 1
            put st 1
            case lookup filepath fs' of
              Nothing ->
                throw ex ("File not found: " <> filepath)
              Just s -> pure s,
          writeFileImpl = \filepath contents ->
            modify fs ((filepath, contents) :)
        }


subsume2L :: (e1 :> es, e2 :> es) => Eff (e1 :& e2 :& es) a -> Eff es a
subsume2L = Unsafe.Coerce.unsafeCoerce

useImpl2 :: (e :> es) => Eff (e1 :& e2 :& e) a -> Eff (e1 :& e2 :& es) a
useImpl2 = Unsafe.Coerce.unsafeCoerce

This seems like it makes interpretations easier while making call sites a little harder.

@tomjaguarpaw
Copy link
Owner

Ah yes, that's also nice. I think it's more important to make interpretations easier, because they tend to be more complex and written more often. Call sites just need to fit a simple standard pattern, and they're only defined once.

What we really need is one simple way of doing all the different forms of this problem. One good thing about your version is that it's compatible with @Lysxia's version. Perhaps we can combine the two and use that as the standard way.

@tomjaguarpaw
Copy link
Owner

I'm pleased to say that subsume2L can be written in a composable way

subsume2 :: (e2 `In` e1) -> (e2 :& e1) `In` e1
subsume2 i = cmp (bimap i (eq (# #))) (merge (# #))

subsume1L :: (e1 :> es) => Eff (e1 :& es) a -> Eff es a
subsume1L = weakenEff (subsume2 has)

subsume2L :: (e1 :> es, e2 :> es) => Eff (e1 :& e2 :& es) a -> Eff es a
subsume2L = subsume1L . subsume1L

I'm not sure about useImpl2 yet.

@tomjaguarpaw
Copy link
Owner

By the way, I think the implementations of readFile and writeFile are already covered by mapHandle.

tomjaguarpaw added a commit that referenced this issue Oct 16, 2024
@tomjaguarpaw
Copy link
Owner

I managed to make this simpler. Turns out it's unnecessary to have separate effect types for each handle argument. No need for subsume2L or useImpl2. We can probably refine this into an even simpler pattern.

data FsWeird es = MkFsWeird
  { readFileImpl ::
      forall e.
      Exception String e ->
      Stream Int e ->
      FilePath ->
      Eff (e :& es) String,
    writeFileImpl ::
      FilePath ->
      String ->
      Eff es ()
  }

instance Handle FsWeird where
  mapHandle MkFsWeird {readFileImpl, writeFileImpl} =
    MkFsWeird
      { readFileImpl =
          \ex sm fp -> insertManySecond (readFileImpl ex sm fp),
        writeFileImpl =
          \fp s -> useImpl (writeFileImpl fp s)
      }

readFile ::
  (e1 :> es, e2 :> es, e3 :> es) =>
  Exception String e1 ->
  Stream Int e2 ->
  FsWeird e3 ->
  FilePath ->
  Eff es String
readFile ex sm MkFsWeird {readFileImpl} fp =
  inContext (readFileImpl (mapHandle ex) (mapHandle sm) fp)

writeFile ::
  (e :> es) => FsWeird e -> FilePath -> String -> Eff es ()
writeFile fs filepath contents =
  useImpl (writeFileImpl fs filepath contents)

runFileSystemPure ::
  (e1 :> es) =>
  State Int e1 ->
  [(FilePath, String)] ->
  (forall e. FsWeird e -> Eff (e :& es) r) ->
  Eff es r
runFileSystemPure st fs0 k =
  evalState fs0 $ \fs ->
    useImplIn
      k
      MkFsWeird
        { readFileImpl = \ex sm filepath -> do
            fs' <- get fs
            yield sm 1
            yield sm 1
            put st 1
            case lookup filepath fs' of
              Nothing ->
                throw ex ("File not found: " <> filepath)
              Just s -> pure s,
          writeFileImpl = \filepath contents ->
            modify fs ((filepath, contents) :)
        }

Commit containing this code

tomjaguarpaw added a commit that referenced this issue Nov 15, 2024
@tomjaguarpaw
Copy link
Owner

This ought to be resolved by: https://hackage.haskell.org/package/bluefin-0.0.11.0/docs/Bluefin-Compound.html#g:8. Please feel free to reopen if you think it's not resolved.

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

2 participants