如何创建IO monad的有限版本

时间:2018-01-09 00:04:24

标签: haskell monad-transformers

我使用monad变换器堆栈编写了许多函数:

data Options
data Result
data Input
type Ingest a = EitherT String (ReaderT Options IO) a

foo :: Input -> Ingest Result

等等。现在,大多数这些功能基本上都是纯粹的。我只需要其中一个函数中的IO:这个函数读取一个文件,并记录它(使用log :: String -> IO ())它已经这样做了。所以这个函数的杂质“感染”了我的整个代码库,使所有这些函数都能够执行IO,即使它们不需要,除了调用这个函数。这有点令人反感,原因有两个:

  • 它不清楚这些可能执行的IO的有限子集
  • 它不清楚哪些函数实际执行IO

一位同事建议在我喜欢的基础monad类型上参数化Ingest。具体来说,定义一个用于读取文件内容的类型类,并为IO提供一个实例,也可能为其他一些monad用于编写测试:

class Monad m => ReadFile m where
  readFile :: FilePath -> m Text

instance ReadFile IO where
  readFile = Data.Text.IO.readFile

日志记录的类型类已经存在,所以我可以使用现有的类型类。但后来我不确定如何使用这个新课程。首先,我用什么替换我的类型别名?我不能写

type Ingest m a = (Logging m, ReadFile m) => EitherT String (ReaderT Options m) a

因为类型同义词不允许使用约束。我必须将这个约束添加到我的每个函数中吗?原则上这很好,因为它标记了可能需要读取文件的函数,但实际上这些函数是相互递归的,因此所有这些函数都需要该权限,这使得将它们全部写出来很痛苦。

我可以定义一个newtype包装器而不是类型同义词,但我认为这不会让事情变得更好:我仍然需要为我的每个函数添加这个新约束。

其次,如何在EitherT / ReaderT堆栈中实际调用新类型类的函数?我不能简单地写

foo :: ReadFile m => FilePath -> Ingest m Text
foo = readFile

因为这会忽略EitherT和ReaderT包装器。我会写这个吗?

foo :: ReadFile m => FilePath -> Ingest m Text
foo = lift . lift $ readFile

看起来有点痛苦并且对变压器堆栈的结构非常脆弱。我是否写了一些像

这样的实例
instance ReadFile m => ReadFile (EitherT e m) where
  readFile = lift readFile

?这似乎也是令人沮丧的样板量。

1 个答案:

答案 0 :(得分:1)

而不是直接使用IO,而是将其包装在摘要 newtype中,例如:

module IOLog(IOLog, logMsg) where 

newtype IOLog a = IOLog (IO a)

instance Functor IOLog where ...

instance Monad IOLog where ...

logMsg :: String -> IOLog ()
logMsg =  logIO . log

 -- local definitions --
 --
logIO :: IO a -> IOLog a
logIO =  IOLog

log :: String -> IO ()
         .
         .
         .

并使用它来定义Ingest

type Ingest a = EitherT String (ReaderT Options IOLog) a

通过这种方式,您可以控制程序其余部分可以使用的I / O子集,而无需支付额外模块的费用-no type-system extensions needed!