使用ReaderT传递IO动作而非函数参数时的类型系统阻碍问题
用Reader Monad转换器传递IO动作时,类型系统的阻碍点解析
问题背景
原本的可测试IO函数采用参数传递依赖的方式,将IO动作作为参数传入,实现生产环境和测试环境的灵活切换:
numCharactersInFile' :: Monad m => (FilePath -> m String) -> FilePath -> m Int numCharactersInFile' f filePath = do contents <- f filePath return (length contents)
使用示例:
module Main where import MTLPrelude main :: IO () main = do -- 生产环境使用 n <- numCharactersInFile' readFile "/home/enrico/fibs.hs" print n -- 测试环境使用 print $ 18 == (runIdentity . numCharactersInFile' mockFileSystem "path") mockFileSystem :: FilePath -> Identity String mockFileSystem _ = return "mock file contents"
为了避免到处传递参数,尝试改用ReaderT层传递readFile,修改后的函数如下:
numCharactersInFile'' :: (MonadIO m, MonadReader (FilePath -> m String) m) => FilePath -> m Int numCharactersInFile'' filePath = do f <- ask contents <- f filePath return (length contents)
但调用时报类型错误:
main :: IO () main = do -- 生产环境使用 i <- numCharactersInFile'' "/home/enrico/fibs.hs" `runReaderT` readFile print i -- 测试环境使用 j <- numCharactersInFile'' "path" `runReaderT` mockFileSystem print $ 18 == j
报错信息:
• Couldn't match type: IO String with: ReaderT (FilePath -> IO String) IO String arising from a functional dependency between: constraint ‘MonadReader (FilePath -> ReaderT (FilePath -> IO String) IO String) (ReaderT (FilePath -> IO String) IO)’ arising from a use of ‘numCharactersInFile''’ instance ‘MonadReader r (ReaderT r m)’ at <no location info> • In the first argument of ‘runReaderT’, namely ‘numCharactersInFile'' "/home/enrico/fibs.hs"’ In a stmt of a 'do' block: i <- numCharactersInFile'' "/home/enrico/fibs.hs" `runReaderT` readFile In the expression: do n <- numCharactersInFile' readFile $ "/home/enrico/fibs.hs" print n print $ 18 == (runIdentity . numCharactersInFile' mockFileSystem $ "path") i <- numCharactersInFile'' "/home/enrico/fibs.hs" `runReaderT` readFile .... [-Wdeferred-type-errors]
核心原因:类型循环依赖
问题出在函数的类型约束MonadReader (FilePath -> m String) m上:
- 当你用
runReaderT ... readFile时,m被实例化为ReaderT (FilePath -> IO String) IO - 此时约束要求Reader的环境是
FilePath -> m String,也就是FilePath -> ReaderT (FilePath -> IO String) IO String - 但你传入的
readFile类型是FilePath -> IO String,和要求的类型完全不匹配,这就导致了类型错误
简单来说:你要求Reader存储的是能返回当前Monad上下文内值的动作,但实际传入的是返回原始IO值的动作,二者的Monad上下文不一致。
修正方案
方案1:适配IO动作的类型
调整类型约束,让Reader存储的是FilePath -> IO String类型的动作,然后在函数内部用liftIO将IO值提升到当前Monad上下文:
numCharactersInFile'' :: (MonadIO m, MonadReader (FilePath -> IO String) m) => FilePath -> m Int numCharactersInFile'' filePath = do f <- ask contents <- liftIO $ f filePath return (length contents)
调用示例:
main :: IO () main = do -- 生产环境 i <- numCharactersInFile'' "/home/enrico/fibs.hs" `runReaderT` readFile print i -- 测试环境 let mock :: FilePath -> IO String mock _ = return "mock file contents" j <- numCharactersInFile'' "path" `runReaderT` mock print $ 18 == j
方案2:支持多Monad场景(需RankNTypes扩展)
如果要同时兼容IO和Identity等Monad场景,可以使用RankNTypes扩展,让Reader存储通用的Monad动作:
{-# LANGUAGE RankNTypes #-} numCharactersInFile''' :: (MonadReader (forall m. Monad m => FilePath -> m String) m) => FilePath -> m Int numCharactersInFile''' filePath = do f <- ask contents <- f filePath return (length contents) main :: IO () main = do -- 生产环境 i <- numCharactersInFile''' "/home/enrico/fibs.hs" `runReaderT` readFile print i -- 测试环境 let j = runIdentity $ numCharactersInFile''' "path" `runReaderT` mockFileSystem print $ 18 == j
内容的提问来源于stack exchange,提问作者Enrico
相关产品推荐
相关产品推荐

