Reputation: 55
I'm working on implementing an embedded DSL in PureScript using the "finally tagless" style. The repo can be found at https://github.com/afcondon/purescript-finally-tagless-ex
Here's the problem. Given an abstract definition of a very simplified file system:
class (Monad m) <= MonadFileSystem m where
cd :: FilePath -> m Unit
ls :: m (Array (Tuple FilePath FileType))
cat :: Array FilePath -> m String
One can easily provide an implementation such as this one (https://github.com/afcondon/purescript-finally-tagless-ex/blob/master/MonadicEx/src/FakeFileSystem.purs) which can be used as an embedded language and interpreted (or run) to evaluate as a string (alternatively you could do static analysis on the structure instead of turning it into a string).
In principle, one can also have a side-effecting example which would actually interact with a file system but which could "interpret" exactly the same embedded language. I'd like to use purescript-node-fs
for this which would mean accepting Eff (fs :: FS, err :: EXCEPTION | eff)
somewhere.
My question is - how does one actually implement the "real", effectful instance? do you have to change the signatures of cd
, ls
and cat
? or can you somehow evaluate the whole monad in Eff
such that those functions do not need to carry the Eff
in their signature?
Upvotes: 3
Views: 298
Reputation: 4649
Because you want to make an instance for Eff
, there's a little problem here since we need to include the row in the type, but as you probably found, the compiler complains about the instance head in that case.
One option is to use a newtype
:
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Exception (EXCEPTION)
import Data.Tuple (Tuple)
import Node.FS (FS)
import Node.Path (FilePath)
type FileType = String
class (Monad m) <= MonadFileSystem m where
cd :: FilePath -> m Unit
ls :: m (Array (Tuple FilePath FileType))
cat :: Array FilePath -> m String
newtype FSEff eff a = FSEff (Eff (fs :: FS, err :: EXCEPTION | eff) a)
runFSEff :: forall eff a. FSEff eff a -> Eff (fs :: FS, err :: EXCEPTION | eff) a
runFSEff (FSEff fse) = fse
derive newtype instance functorFSEff :: Functor (FSEff eff)
derive newtype instance applyFSEff :: Apply (FSEff eff)
derive newtype instance applicativeFSEff :: Applicative (FSEff eff)
derive newtype instance bindFSEff :: Bind (FSEff eff)
derive newtype instance monadFSEff :: Monad (FSEff eff)
instance monadFileSystemFSEff :: MonadFileSystem (FSEff eff) where
cd _ = pure unit
ls = pure []
cat _ = pure "meow"
But there is also some trickery that can be done using functional dependencies, where you can specify the row like an equality constraint instead. This compiles, but I've not tried using this technique for real yet, so I can't vouch for it definitely working in a larger context:
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Exception (EXCEPTION)
import Data.Tuple (Tuple)
import Node.FS (FS)
import Node.Path (FilePath)
type FileType = String
class (Monad m) <= MonadFileSystem m where
cd :: FilePath -> m Unit
ls :: m (Array (Tuple FilePath FileType))
cat :: Array FilePath -> m String
instance monadFileSystemEff :: EffectRowEquals r (fs :: FS, err :: EXCEPTION | eff) => MonadFileSystem (Eff r) where
cd _ = pure unit
ls = pure []
cat _ = pure "meow"
-- This should probably go in `purescript-type-equality`:
class EffectRowEquals (a ∷ # !) (b ∷ # !) | a → b, b → a where
toER ∷ ∀ r. r a → r b
fromER ∷ ∀ r. r b → r a
instance reflER ∷ EffectRowEquals r r where
toER er = er
fromER er = er
Upvotes: 3