packages feed

heftia-effects-0.5.0.0: Example/NonDet/Main.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fplugin GHC.TypeLits.KnownNat.Solver #-}

-- SPDX-License-Identifier: MPL-2.0

module Main where

import Control.Monad.Hefty (
    Eff,
    interpret,
    liftIO,
    makeEffectF,
    runEff,
    (&),
    type (<:),
    type (<|),
    type (~>),
 )
import Control.Monad.Hefty.Except (Throw, joinEither, runThrowIO, throw)
import Control.Monad.Hefty.NonDet (Choose, Empty, choice, runNonDetMonoid)
import Data.Function (fix)
import Data.Functor ((<&>))
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Monoid (Sum (Sum))
import System.FilePath (splitDirectories, (</>))
import UnliftIO (Exception)

-- | Effect for file system operations
data FileSystem a where
    ListDirectory :: FilePath -> FileSystem (Either NotADir [FilePath])
    GetFileSize :: FilePath -> FileSystem (Either NotAFile Integer)

-- | Exception for when a directory was expected but found a file
data NotAFile = NotAFile

-- | Exception for when a file was expected but found a directory
data NotADir = NotADir
    deriving (Show)
    deriving anyclass (Exception)

makeEffectF [''FileSystem]

-- | Exception for when an entry does not exist at the specified path
data EntryNotFound = EntryNotFound
    deriving (Show)
    deriving anyclass (Exception)

-- | Aggregate the sizes of all files under the given path
totalFileSize
    :: (Choose <| ef, Empty <| ef, FileSystem <| ef, Throw NotADir <| ef, IO <| ef)
    => FilePath
    -> Eff '[] ef (Sum Integer)
totalFileSize path = do
    entities :: [FilePath] <- listDirectory path & joinEither
    entity :: FilePath <- choice entities -- Non-deterministically /pick/ one item from the list
    let path' = path </> entity

    liftIO $ putStrLn $ "Found " <> path'

    getFileSize path' >>= \case
        Right size -> do
            liftIO $ putStrLn $ " ... " <> show size <> " bytes"
            pure $ Sum size
        Left NotAFile -> do
            totalFileSize path'

main :: IO ()
main = runEff
    . runThrowIO @EntryNotFound
    . runThrowIO @NotADir
    . runDummyFS exampleRoot
    $ do
        total <- runNonDetMonoid pure (totalFileSize ".")
        liftIO $ print total

{-
>>> main
Found ./README.md
 ... 4000 bytes
Found ./src
Found ./src/Bar.hs
 ... 1000 bytes
Found ./src/Foo.hs
 ... 2000 bytes
Found ./test
Found ./test/Baz.hs
 ... 3000 bytes
Sum {getSum = 10000}
-}

-- | Example directory structure used this time
exampleRoot :: FSTree
exampleRoot =
    dir
        [
            ( "."
            , dir
                [
                    ( "src"
                    , dir
                        [ ("Bar.hs", File 1000)
                        , ("Foo.hs", File 2000)
                        ]
                    )
                ,
                    ( "test"
                    , dir
                        [ ("Baz.hs", File 3000)
                        ]
                    )
                , ("README.md", File 4000)
                ]
            )
        ]
  where
    dir :: [(FilePath, FSTree)] -> FSTree
    dir = Dir . Map.fromList

-- | Directory structure
data FSTree
    = Dir {entries :: Map FilePath FSTree}
    | File {fileSize :: Integer}

{- |
Interpreter for the FileSystem effect that virtualizes the file system in memory
based on a given FSTree, instead of performing actual IO.
-}
runDummyFS
    :: (Throw EntryNotFound <| ef, Throw NotADir <| ef)
    => FSTree
    -> Eff eh (FileSystem ': ef) ~> Eff eh ef
runDummyFS root = interpret \case
    ListDirectory path ->
        lookupFS path root <&> \case
            Dir entries -> Right $ Map.keys entries
            File _ -> Left NotADir
    GetFileSize path ->
        lookupFS path root <&> \case
            File size -> Right size
            Dir _ -> Left NotAFile

-- | Lookup the directory structure by path
lookupFS
    :: (Throw EntryNotFound <: m, Throw NotADir <: m, Monad m)
    => FilePath
    -> FSTree
    -> m FSTree
lookupFS path =
    splitDirectories path & fix \dive -> \case
        [] -> pure
        dirName : restPath -> \case
            Dir currentDir -> do
                case currentDir Map.!? dirName of
                    Just restTree -> dive restPath restTree
                    Nothing -> throw EntryNotFound
            File _ -> throw NotADir