Closed JeanHuguesdeRaigniac closed 1 year ago
This problem stems from m
being ambiguous in your call to modify
(there wasn't any context from which the compiler can infer what m
exactly is). Consider supplying the type of the state via TypeApplications
:
reinterpret \case
ReadFile path -> do
void $ modify @[FSWrapper Identity] (S (ReadFile path) :)
pure "a"
WriteFile path contents -> modify @[FSWrapper Identity] (V (WriteFile path contents) :)
(Also there's cleff-plugin
that tries to disambiguate cases like this automatically, but it only works up to GHC 9.2; I haven't got the time to make it work with 9.4 yet.)
Thanks, that was fast!
My project uses GHC 9.2.5 but I wanted to try on a small scale: this test is done within GHCi on your repo.
Do you know if there is a way to avoid this FSWrapper
please?
Not completely avoid, but this could be less cumbersome:
data FSWrapper = forall m a. Op (Filesystem m a)
This type also gets rid of needing to explicitly supply the state type in your modify
.
Thanks, it covers well type construction but there seem to be no way to pattern match on the command used:
*Main> test = runPure $ runFS $ readFile "nonexistent"
*Main> [Op cmd] = test
<interactive>:121:5: error:
• Couldn't match expected type ‘p’
with actual type ‘Filesystem m a’
because type variables ‘m’, ‘a’ would escape their scope
These (rigid, skolem) type variables are bound by
a pattern with constructor:
Op :: forall (m :: Type -> Type) a. Filesystem m a -> FSWrapper,
in a pattern binding
at <interactive>:121:2-7
• In the pattern: Op cmd
In the pattern: [Op cmd]
In a pattern binding: [Op cmd] = test
*Main> [Op (ReadFile p :: Filesystem m String)] = test
<interactive>:120:6: error:
• You cannot bind scoped type variable ‘m’
in a pattern binding signature
• In the pattern: ReadFile p :: Filesystem m String
In the pattern: Op (ReadFile p :: Filesystem m String)
In the pattern: [Op (ReadFile p :: Filesystem m String)]
<interactive>:120:6: error:
• Couldn't match type ‘a’ with ‘String’
‘a’ is a rigid type variable bound by
a pattern with constructor:
Op :: forall (m :: Type -> Type) a. Filesystem m a -> FSWrapper,
in a pattern binding
at <interactive>:120:2-39
Expected type: Filesystem m a
Actual type: Filesystem m String
• When checking that the pattern signature: Filesystem m String
fits the type of its context: Filesystem m a
In the pattern: ReadFile p :: Filesystem m String
In the pattern: Op (ReadFile p :: Filesystem m String)
For the record, here is another shot at it for State
and Writer
. Still a bit verbose but more ergonomic.
There is certainly better but it will do for now.
module Main where
import Cleff
import Cleff.State
import Cleff.Writer
import Control.Monad (void)
import Data.Functor.Identity
import Prelude hiding (readFile, writeFile)
data Filesystem :: Effect where
ReadFile :: FilePath -> Filesystem m String
WriteFile :: FilePath -> String -> Filesystem m ()
makeEffect ''Filesystem
data FSWrapper m = S (Filesystem m String) | V (Filesystem m ())
type FSWrapperI a = FSWrapper Identity
instance Eq (FSWrapper Identity) where
S (ReadFile path) == S (ReadFile path') = path == path'
S _ == _ = False
V (WriteFile path contents) == V (WriteFile path' contents') =
path == path' && contents == contents'
V _ == _ = False
-- State
wrap :: Filesystem m a -> FSWrapperI a
wrap (ReadFile path) = S (ReadFile path)
wrap (WriteFile path contents) = V (WriteFile path contents)
runFS :: Eff (Filesystem : es) a -> Eff es [FSWrapper Identity]
runFS =
fmap snd
. runState []
. reinterpret \case
ReadFile path -> do
void $ modify (wrap (ReadFile path) :)
pure "state"
WriteFile path contents -> modify (wrap (WriteFile path contents) :)
-- Writer
wrap' :: Filesystem m a -> [FSWrapperI a]
wrap' (ReadFile path) = [S (ReadFile path)]
wrap' (WriteFile path contents) = [V (WriteFile path contents)]
runFS' :: Eff (Filesystem : es) a -> Eff es [FSWrapper Identity]
runFS' =
fmap snd
. runWriter
. reinterpret \case
ReadFile path -> do
void $ tell $ wrap' (ReadFile path)
pure "writer"
WriteFile path contents -> tell $ wrap' (WriteFile path contents)
main :: IO ()
main =
do
-- State
let [V (WriteFile path content)] = runPure $ runFS $ writeFile "path" "state"
print $ path == "path" && content == "state"
let cmds = runPure $ runFS $ readFile "nonexistent"
print $ wrap (ReadFile "nonexistent") `elem` cmds
-- Writer
let [S (ReadFile path')] = runPure $ runFS' $ readFile "nonexistent"
print $ path' == "nonexistent"
let cmds' =
runPure $
runFS' $ do
content' <- readFile "nonexistent"
writeFile "path" content'
print $ wrap (WriteFile "path" "writer") `elem` cmds'
Thanks, it covers well type construction but there seem to be no way to pattern match on the command used
You could probably case
on it (as opposed to creating a binding) and be careful not to return any value whose type depends on the existential m
from the case expression.
Of course, you can probably also just do this for Filesystem
, which is a first-order effect whose operations do not depend on the m
parameter:
data FsWrapper = forall a. Op (Filesystem Identity a)
For tests purpose, I try to make an interpreter which stores used commands of an effect. Here I tried with a
State
but of course result is the same with aWriter
.I had some overlapping instances errors if I stored directly the commands (
String
fromReadFile
is not()
fromWriteFile
), so I added aFSWrapper
type to hide their return types. But I still get overlapping problem over the monad now.Here is a way to reproduce it:
It produces this error:
What am I supposed to do?