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

Documentation about composing IO effects #12

Open shmish111 opened 6 years ago

shmish111 commented 6 years ago

IMO one common use case for this library is to have an algebra of effects over some IO actions. You never end up with only one effect using IO. For example you may need to wrap file handling in an effect and also spawning external processes. I couldn't find out how to compose the interpretation of these in any of the docs and since this is a new library there are few other resources. In the end I was able to compose interpretM functions for each however it required quit a few type annotations to get it to work. Something like this:

-- this is in my Main module

interpretIO ::
     (LastMember IO effs, Members '[ IO] effs)
  => Eff (Console ': File ': Process ': Error AppError ': effs) a
  -> Eff effs (Either AppError a)
interpretIO =
  runError . Process.interpretIO . File.interpretIO . Console.interpretIO

runApp :: Spec -> IO ()
runApp spec = do
  eRes <- runM . interpretIO . runSpec $ spec
  case eRes of
    Left (ProcessFailed ec) -> exitWith $ ExitFailure ec
    Right res          -> pure ()

Where File.interpretIO is:

interpretIO ::
     (LastMember IO effs, Members '[ IO] effs)
  => Eff (File ': effs) a
  -> Eff effs a
interpretIO =
  interpretM
    (\case
       WriteFile path text -> Text.writeFile path text
       ReadFile path -> tryReadFile path
       CreateDirectoryIfMissing parents path ->
         Dir.createDirectoryIfMissing parents path
       GetHomeDirectory -> Dir.getHomeDirectory
       GetCurrentDirectory -> Dir.getCurrentDirectory)

As an example of the type inference problem, I was forced to create interpretIO in Main since eRes <- runM . runError . Process.interpretIO . File.interpretIO . Console.interpretIO . runSpec $ spec would not type check, there were issues with constraints.

Of course I may be doing this completely the wrong way but I need some help from the documentation on this :-)

theGhostJW commented 6 years ago

Hi I am starting to dig into this stuff as well. In this example there are 2 effects ensure (which should be rewritten terms of Error but is just being used as a second effect) and file system.

Here is my solution:


{- File System Lang -}

data FileSystem r where
  ReadFile :: Path a File -> FileSystem StrictReadResult
  WriteFile :: Path a File -> String -> FileSystem ()

readFile :: Member FileSystem effs => Path a File -> Eff effs StrictReadResult
readFile = send . ReadFile

writeFile :: Member FileSystem effs => Path a File -> String -> Eff effs ()
writeFile pth = send . WriteFile pth

{- Ensure Lang -}

data AppEnsure r where
 Ensure :: Bool -> String -> AppEnsure ()
 FailEn :: String -> AppEnsure ()

ensure :: Member AppEnsure effs => Bool -> String -> Eff effs ()
ensure condition message = send $ Ensure condition message

failEn :: Member AppEnsure effs =>  String -> Eff effs ()
failEn = send . FailEn

{- File System IO Interpreter -}

fileSystemIOInterpreter :: forall effs a. LastMember IO effs => Eff (FileSystem ': effs) a -> Eff effs a
fileSystemIOInterpreter = interpretM $ \case
                               ReadFile path -> F.readFileUTF8 path
                               WriteFile path str -> F.writeFileUTF8 path str

{- Ensure IO Interpreter -}

ensureIOInterpreter :: forall effs a. LastMember IO effs => Eff (AppEnsure ': effs) a -> Eff effs a
ensureIOInterpreter = interpretM $ \case
                                  Ensure condition errMsg -> Monad.unless condition $ Monad.fail $ toList errMsg
                                  FailEn errMsg -> Monad.fail $ toList errMsg

{- Application (Interactor) -}

data ApState = ApState {
  filePath :: Path Abs File,
  fileText :: StrictReadResult
}
  deriving Show

data TestItem = Item {
  pre :: String,
  post :: String,
  path :: Path Abs File
}

data RunConfig = RunConfig {
  environment :: String,
  depth :: Integer,
  path :: Path Abs File
}

interactor :: Members '[AppEnsure, FileSystem] effs => TestItem -> RunConfig -> Eff effs ApState
interactor item runConfig = do
                              let fullFilePath = path (runConfig :: RunConfig)
                              writeFile fullFilePath $ pre item  <> " ~ " <> post item <> " !!"
                              -- failEn "random error ~ its a glitch"
                              txt <- readFile fullFilePath
                              pure $ ApState fullFilePath txt

{- Application IO Interpreter -}

executeInIO :: forall a. Eff '[FileSystem, AppEnsure, IO] a -> IO a
executeInIO app = runM $ ensureIOInterpreter
                       $ fileSystemIOInterpreter
                       app

{- Demo Execution -}

sampleItem =  Item {
  pre = "I do a test",
  post = "the test runs",
  path = [absfile|C:\Vids\SystemDesign\VidList.txt|]
}

sampleRunConfig = RunConfig {
  environment = "Test",
  depth = 44,
  path = [absfile|C:\Vids\SystemDesign\VidList.txt|]
}

-- Demos
demoExecuteInIO = executeInIO $ interactor sampleItem sampleRunConfig
lexi-lambda commented 6 years ago

@shmish111 I cannot reproduce your problem. The code that you provide is not complete, so it is not runnable, but I tried to reduce what you provided to a minimal test case. However, what I ended up with typechecks. This is my program:

import Control.Monad.Freer
import Control.Monad.Freer.Error

data AppError = ProcessFailed

data Console r
data File r
data Process r

processInterpretIO :: LastMember IO effs => Eff (Process ': effs) ~> Eff effs
processInterpretIO = undefined
fileInterpretIO :: LastMember IO effs => Eff (File ': effs) ~> Eff effs
fileInterpretIO = undefined
consoleInterpretIO :: LastMember IO effs => Eff (Console ': effs) ~> Eff effs
consoleInterpretIO = undefined

data Spec

runSpec :: Members '[Console, File, Process, Error AppError] effs => Spec -> Eff effs ()
runSpec = undefined

runApp :: Spec -> IO ()
runApp spec = do
  eRes <- runM . runError . processInterpretIO . fileInterpretIO . consoleInterpretIO . runSpec $ spec
  case eRes of
    Left ProcessFailed -> pure ()
    Right _ -> pure ()

Is something different about your program that makes it fail to typecheck?

shmish111 commented 6 years ago

@lexi-lambda the issue was that I wasn't including forall a. or using ~> which implies it. I think the title of the issue is relevant here, what I needed was documentation around this, i.e. I should have used ~> and why. I also think I understand why this is but a brief explanation would also be nice.

lexi-lambda commented 6 years ago

Could you post the type you tried assigning to Process.interpretIO and friends? I’d like to better understand your misunderstanding.