【问题标题】:How to create a limited version of the IO monad如何创建有限版本的 IO monad
【发布时间】:2018-01-09 00:04:24
【问题描述】:

我已经使用 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

一位同事建议将 Ingest 参数化为我喜欢的基本 monad 类型。具体来说,定义一个用于读取文件内容的类型类,并为 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

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

我可以定义一个新类型包装器而不是类型同义词,但我认为这并没有让事情变得更好:我仍然必须将这个新约束添加到我的每个函数中。

其次,我如何从 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

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

【问题讨论】:

  • 我投了“不清楚你在问什么”,因为这看起来更像是一个挫折日志。您有任何具体问题,或者您想在codereview 上发布一些代码(如果您联系我,我很乐意查看并提出建议)?
  • 以 mtl 风格全押。写type Ingest m = (Logging m, ReadFile m, MonadReader Options m, MonadError String m),然后写Ingest m => m a而不是Ingest m a
  • @AlexisKing 我不知道你可以使用type 来定义约束同义词。是新的吗?
  • @amalloy 不,使用 ConstraintKinds 语言扩展名总是允许的,自 GHC 7.4.1 起就可用。

标签: haskell monad-transformers


【解决方案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!

【讨论】:

    猜你喜欢
    • 2019-11-12
    • 2021-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-29
    • 2011-09-29
    • 1970-01-01
    • 2016-08-12
    相关资源
    最近更新 更多