packages feed

effet-0.3.0.2: examples/Example/FileSystem.hs

{-# LANGUAGE TemplateHaskell #-}
-- | Example of a custom effect for file systems access.
module Example.FileSystem where

-- base
import Data.Coerce    (coerce)
import Data.Maybe     (fromMaybe)
import Prelude hiding (lookup, readFile, writeFile)
import qualified System.IO as IO

-- hspec
import Test.Hspec (Spec, it, shouldBe)

-- effet
import Control.Effect.Identity
import Control.Effect.Machinery
import Control.Effect.Map
import Control.Effect.Map.Strict

import Example.Logger
import Hspec (shouldPrint)

--- Custom Effects -------------------------------------------------------------

-- | An effect for representing file system access.
class Monad m => FileSystem m where
  readFile  :: FilePath -> m String
  writeFile :: FilePath -> String -> m ()

makeEffect ''FileSystem

--- Effect Interpretations -----------------------------------------------------

-- | An effect handler for 'FileSystem' which touches the actual file system.
newtype LocalFS m a =
  LocalFS { runLocalFS :: m a }
    deriving (Applicative, Functor, Monad, MonadIO)
    deriving (MonadTrans, MonadTransControl) via IdentityT
    deriving (MonadBase b, MonadBaseControl b)

instance MonadIO m => FileSystem (LocalFS m) where
  readFile path = liftIO $ IO.readFile path
  writeFile path txt = liftIO $ IO.writeFile path txt

runLocalFileSystem :: (FileSystem `Via` LocalFS) m a -> m a
runLocalFileSystem = coerce

-- | An effect handler for 'FileSystem' which provides a virtual file system.
newtype VirtualFS m a =
  VirtualFS { runVirtualFS :: m a }
    deriving (Applicative, Functor, Monad, MonadIO)
    deriving (MonadTrans, MonadTransControl) via IdentityT
    deriving (MonadBase b, MonadBaseControl b)

instance Map FilePath String m => FileSystem (VirtualFS m) where
  readFile path = VirtualFS $ fromMaybe "" <$> lookup path
  writeFile path txt = VirtualFS $ insert path txt

runVirtualFileSystem :: (FileSystem `Via` VirtualFS) m a -> m a
runVirtualFileSystem = coerce

-- | An effect handler for 'FileSystem' which intercepts the access to files
-- and logs the actions using the 'Logger' effect.
newtype Interceptor m a =
  Interceptor { unInterceptor :: m a }
    deriving (Applicative, Functor, Monad, MonadIO)
    deriving (MonadTrans, MonadTransControl) via IdentityT
    deriving (MonadBase b, MonadBaseControl b)

instance (FileSystem m, Logger m) => FileSystem (Interceptor m) where
  readFile path = Interceptor $
    logStr ("READ: " ++ path) >> readFile path
  writeFile path txt = Interceptor $
    logStr ("WRITE: " ++ path ++ " [" ++ txt ++ "]") >> writeFile path txt

runInterceptor :: (FileSystem `Via` Interceptor) m a -> m a
runInterceptor = coerce

--- Example Programs -----------------------------------------------------------

-- | Simple program which writes a file and reads it afterwards and returns the
-- old and new file content.
writeRead :: FileSystem m => FilePath -> String -> m (String, String)
writeRead path txt = do
  old <- readFile path
  writeFile path txt
  new <- readFile path
  pure (old, new)

-- | Appends a text to a file given by a specific file path, and returns the old
-- and new lengths of the file.
append :: FileSystem m => FilePath -> String -> m (Int, Int)
append path txt = do
  content <- readFile path
  let size = length content
  seq size $ writeFile path (content ++ txt)
  pure (size, size + length txt)

--- Test Cases -----------------------------------------------------------------

spec :: Spec
spec = do
  it "evaluates writeRead virtually" $
    ( runIdentity             -- result:  (String, String)
    . runMap                  -- result:  Monad m => m (String, String)
    . runVirtualFileSystem    -- effects: Map FilePath String
    $ writeRead "tmp" "hello" -- effects: FileSystem
    ) `shouldBe` ("", "hello")
  it "evaluates append virtually" $
    ( runIdentity             -- result:  (Int, Int)
    . runMap                  -- result:  Monad m => m (Int, Int)
    . runVirtualFileSystem    -- effects: Map FilePath String
    $ append "tmp" "hello"    -- effects: FileSystem
    ) `shouldBe` (0, 5)
  it "intercepts append by printing" $
    ( runPrintLogger          -- result:  MonadIO m => m (), unified with IO ()
    . runMap                  -- effects: Logger
    . runVirtualFileSystem    -- effects: Map FilePath String, Logger
    . runInterceptor          -- effects: FileSystem, Logger
    $ append "tmp" "hello"    -- effects: FileSystem
    ) `shouldPrint` "\"READ: tmp\"\n\"WRITE: tmp [hello]\"\n"
  it "intercepts append by collecting" $
    ( runIdentity             -- result:  ((Int, Int), [String])
    . runCollectLogger        -- result:  Monad m => m ((Int, Int), [String])
    . runMap                  -- effects: Logger
    . runVirtualFileSystem    -- effects: Map FilePath String, Logger
    . runInterceptor          -- effects: FileSystem, Logger
    $ append "tmp" "hello"    -- effects: FileSystem
    ) `shouldBe` ((0, 5), ["WRITE: tmp [hello]", "READ: tmp"])