简体   繁体   中英

How to implement Monadic instance of a “finally tagless” typeclass in PureScript?

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?

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM