State
作为 IO
的替代品采用这样一个简单的函数
foo :: IO ()
foo = putStrLn "ciao"
这是不可测试的,因为它会影响终端的状态,我无法为其编写测试。
使用here建议的方法,可以用受类型
m
约束的通用单子class
重写上述内容,例如名为IOLike
,这需要实现者,其中之一就是IO
本身,定义 putStrLn
。
在代码中这意味着我们定义了这个
class
,
import Prelude hiding (putStrLn)
class IOLike m where
putStrLn :: String -> m ()
编写
IO
, 的简单实现
import Prelude hiding (putStrLn)
import qualified Prelude as P (putStrLn)
instance IOLike IO where
putStrLn = P.putStrLn
为
State
monad 编写所需的实现,用于在测试中模仿 IO
,
import MTLPrelude
instance IOLike (State String) where
putStrLn str = modify' $ \s -> s ++ str
上述代码到位后,我们可以将
foo
的定义更改为
foo :: IOLike m => m ()
foo = putStrLn "ciao"
现在可以通过生产代码在
IO
monad 中使用,
main :: IO ()
main = do
foo
并在
State
monad 中通过测试代码
main :: IO ()
main = do
P.putStrLn $ foo `execState` ""
其中
""
是表示终端状态的字符串的初始状态。 P.putStrLn
应该更改为例如通过 QuickCheck
做出的某些断言。
到目前为止一切都很好,但是...
State
作为基于 IO
的单子堆栈的替代方案?但是现在假设我们有另一个函数,
bar
,它意味着在更复杂的 monad 中运行,并在单子堆栈的底部有一个 MonadIO
,例如
bar :: (MonadIO m, MonadReader String m) => m ()
bar = do
e <- ask
liftIO $ P.putStrLn $ "ciao" ++ show e
它将在
IO
monad 中运行,
main :: IO ()
main = do
bar `runReaderT` "env"
再次,我想更改
bar
,以便它可以在任何 IOLike
monad 中运行:
bar :: (IOLike m, MonadReader String m) => m ()
bar = do
e <- ask
putStrLn $ "ciao" ++ show e
问题是上面的代码甚至无法在
IO
monad 中运行,
main :: IO ()
main = do
bar `runReaderT` "env"
因为它会出现
No instance for ‘IOLike (ReaderT String IO)’
错误,更不用说尝试在 State
monad 中运行它了,在测试代码中,IO
, 的角色
main :: IO ()
main = do
P.putStrLn $ bar `runReaderT` "env" `execState` ""
因为这也有错误,
No instance for ‘IOLike (ReaderT String (StateT String Identity))’
。
很明显,问题在于让
State String
实现 IOLike
并不意味着任何带有 StateT String m
层的单子堆栈也是 IOLike
。
我想解决方案包括编写一个
instance
来表达任何具有 StateT String
层的单子堆栈都是 IOLike
的想法,但我该怎么做呢?
我尝试将我为
instance
编写的原始 State String
更改为以下内容:
instance (MonadState String m) => IOLike m where
putStrLn str = modify' $ \s -> s ++ str
但是我得到的是这个错误:
• The constraint ‘MonadState String m’
is no smaller than the instance head ‘IOLike m’
• In the instance declaration for ‘IOLike m’
• Perhaps you intended to use UndecidableInstances
如果你保留原来的
instance IOLike (State String)
,但也添加这个,那么它就可以了:
instance (Monad m, IOLike m) => IOLike (ReaderT r m) where
putStrLn = lift . putStrLn