re-xyr / cleff

Fast and concise extensible effects
https://hackage.haskell.org/package/cleff
BSD 3-Clause "New" or "Revised" License
104 stars 6 forks source link

Direct style effects #31

Open tomjaguarpaw opened 6 months ago

tomjaguarpaw commented 6 months ago

This "direct style" approach to defining effects seems simpler than the one in the "Defining effects section". It avoids the GADT whose only purpose is to provide "hooks" onto which to hang the effectful operations. Instead, why not just define the effectful operations directly?

What do you think? Is this a viable approach?

{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}

import Cleff
import Cleff.Fail (Fail)
import Cleff.State (State, gets, modify)
import Data.Kind (Type)
import Data.Map (Map, lookup, insert)
import Prelude hiding (lookup)

-- Direct encoding of the effects we want in our filesystem
data Filesystem m = MkFilesystem
  { readFileFS :: FilePath -> m String,
    writeFileFS :: FilePath -> String -> m ()
  }

-- A type class for such direct-style effects
class EffectDirect (e :: (Type -> Type) -> Type) where
  fmapEffect :: (m ~> n) -> e m -> e n

-- Instance for the filesystem effect
instance EffectDirect Filesystem where
  fmapEffect f MkFilesystem {readFileFS = r, writeFileFS = w} =
    MkFilesystem {readFileFS = f . r, writeFileFS = \s -> f . w s}

-- Adapt direct effects for the existing cleff type-level encoding
data Direct f (m :: Type -> Type) (a :: Type) = Direct (f m -> m a)

-- Direct version of send
sendDirect ::
  (Direct f :> es) => (f (Eff es) -> Eff es a) -> Eff es a
sendDirect = send . Direct

-- Define the effectful operations using sendDirect
readFile'      ::
  Direct Filesystem :> es => FilePath -> Eff es String
readFile'  x   =  sendDirect (\f -> readFileFS f x)
writeFile'     ::
  Direct Filesystem :> es => FilePath -> String -> Eff es ()
writeFile' x y =  sendDirect (\f -> writeFileFS f x y)

-- To run in IO, just define the effects directly
runFilesystemIO ::
  (IOE :> es) =>
  Eff (Direct Filesystem : es) a ->
  Eff es a
runFilesystemIO =
  interpretDirectIO
    MkFilesystem
      { readFileFS = readFile,
        writeFileFS = writeFile
      }

-- To run in State, just define the effects directly
filesystemToState ::
  (Fail :> es) =>
  Eff (Direct Filesystem : es) a ->
  Eff (State (Map FilePath String) : es) a
filesystemToState =
  reinterpretDirect
    MkFilesystem
      { readFileFS = \path ->
          gets (lookup path) >>= \case
            Nothing -> fail ("File not found: " ++ show path)
            Just contents -> pure contents,
        writeFileFS = \path contents ->
          modify (insert path contents)
      }

interpretDirectIO ::
  (IOE :> es, EffectDirect f) =>
  f IO ->
  Eff (Direct f : es) a ->
  Eff es a
interpretDirectIO = interpretDirect . fmapEffect liftIO

interpretDirect ::
  (EffectDirect f) =>
  f (Eff es) ->
  Eff (Direct f : es) a ->
  Eff es a
interpretDirect p = interpret \(Direct f) ->
  withFromEff (\k -> f (fmapEffect k p))

reinterpretDirect ::
  (EffectDirect f) =>
  f (Eff (e' : es)) ->
  Eff (Direct f : es) a ->
  Eff (e' : es) a
reinterpretDirect p = reinterpret \(Direct f) ->
  withFromEff (\k -> f (fmapEffect k p))
tomjaguarpaw commented 6 months ago

By the way, this was prompted by the talk Wire all the things! by Eric Torreborre (Lambda Days 2023) where he says of effect systems

You have to introduce a separation, you cannot use functions directly any more, you have to use data types that are representing your functions. And at large, that makes the code a bit hard to navigate.

I guessed that wasn't true, and lo and behold, after some work, it isn't!

arybczak commented 5 months ago

Does this work for higher order effects? EffectDirect looks annoying to write and it looks like it won't be enough for higher order, i.e. you would also need the reverse mapping

tomjaguarpaw commented 5 months ago

I doubt it works for higher order effects. But I'm also confused about what higher order effects are. As far as I can tell, they're just handlers. So perhaps dynamic higher order effects are dynamic handers? I've never wanted one of those. I wonder what they are for.

tomjaguarpaw commented 5 months ago

I doubt it works for higher order effects.

Actually, I take that back. Since a higher order effect is still just a function defined in terms of the current effect set, the direct encoding should be able to do that too. I still don't understand what higher order effects are for (versus just handlers) though.

arybczak commented 5 months ago
data Foo :: Effect where
  Foo :: m a -> Foo m a

This is an example of a higher order effect. In your proposed encoding:

data Foo m = MkFoo
  { foo :: forall a. m a -> m a
  }

Their operations have effectful actions in the negative position.

tomjaguarpaw commented 5 months ago

Thanks. I understand the definition; I don't understand the purpose. To me, foo looks like a handler, not an effect.

arybczak commented 5 months ago

I'm confused. Reader is higher order because of local. Writer is higher order because of listen. State is higher order if you include StateM e.g. for exclusive state access if the underlying handler uses MVar.

tomjaguarpaw commented 5 months ago

Right, I'm saying I don't understand why local, listen and StateM are useful. If we want to continue the discussion maybe we should do so at the Bluefin issue tracker because this is becoming tangential to cleff.

re-xyr commented 5 months ago

Sorry for not getting to this earlier; college got me extremely preoccupied.

The difference between a higher-order operation and a handler is that the former is an abstraction of the latter, just like how a (first-order) effect operation is an abstraction of a monadic action.

I will take a look at Bluefin some time; it seems interesting!

tomjaguarpaw commented 5 months ago

The difference between a higher-order operation and a handler is that the former is an abstraction of the latter, just like how a (first-order) effect operation is an abstraction of a monadic action.

@re-xyr Thanks. That's in line with what I was anticipating. I haven't yet understood the need to abstract over handlers. I will continue to give it consideration.

@evanrelf I see you gave my comment a "thumbs down". Is there something you could elaborate on? Are you upset with my lack of understanding, or my suggestion to move the discussion elsewhere? (If @re-xyr is happy to continue the discussion here, then that's fine by me. I just don't want to flood someone else's issue tracker with tangential discussion.)

tomjaguarpaw commented 5 months ago

I'm going to continue the discussion about higher-order effects at https://github.com/tomjaguarpaw/bluefin/issues/15.