我有以下小程序:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad.State.Strict
import Control.Monad.Except
import qualified Data.Text as T
import qualified Data.Text.Read as T
import qualified Data.Text.IO as T
main :: IO ()
main = do
_ <- execStateT loop 0
return ()
loop :: StateT Int IO ()
loop = do
liftIO $ putStrLn "Enter a Number"
line <- liftIO $ T.getLine
let ts = T.words line
checkFoo ts
loop
checkFoo :: (MonadState Int m, MonadIO m) => [T.Text] -> m()
checkFoo strs = liftIO (runExceptT
(check1 strs >>= checkNum >>= doFoo)) >>= result
where
doFoo n = liftIO $ putStrLn $ "Your number: " <> show n
check1 :: [a] -> ExceptT T.Text IO a
check1 ts =
if length ts == 1
then return $ head ts
else throwError "1 number please"
checkNum :: T.Text -> ExceptT T.Text IO Int
checkNum t = case T.decimal t of
Left _ -> throwError "input isn't a number"
Right (d, _) -> return $ d
result :: (MonadState Int m, MonadIO m) => Either T.Text () -> m ()
result (Left e) = liftIO $ T.putStrLn e
result (Right _) = return ()
现在我想在函数checkFoo
的子函数doFoo
中访问状态单子的值。如:
doFoo n =
old <- lift get
let s = old + n
liftIO $ putStrLn $ "The sum of your numbers: " <> show s
lift $ put s
pure ()
我得到以下错误:
Main.hs:26:35: error:
• Could not deduce (MonadState a0 IO) arising from a use of ‘doFoo’
from the context: (MonadState Int m, MonadIO m)
bound by the type signature for:
checkFoo :: forall (m :: * -> *).
(MonadState Int m, MonadIO m) =>
[T.Text] -> m ()
at Main.hs:24:1-60
The type variable ‘a0’ is ambiguous
• In the second argument of ‘(>>=)’, namely ‘doFoo’
In the first argument of ‘runExceptT’, namely
‘(check1 strs >>= checkNum >>= doFoo)’
In the first argument of ‘liftIO’, namely
‘(runExceptT (check1 strs >>= checkNum >>= doFoo))’
|
26 | (check1 strs >>= checkNum >>= doFoo)) >>= result
为什么不工作?有什么必要的改变使其工作?
问题是当你写表达式:
check1 strs >>= checkNum >>= doFoo
这要求这些操作中的每一个都是同一个单子的操作。前两个的单子是:
ExceptT T.Text IO
表示doFoo
的类型签名:
doFoo :: Int -> ExceptT T.Text IO ()
但随后您尝试在修订的doFoo
定义中解除put
和get
操作。错误消息告诉您这些操作不被无状态单子ExceptT T.Text IO
所支持。
最不具破坏性的修复可能是修改check1
和checkNum
的类型签名,将它们泛化到任何MonadIO m
上:
check1 :: (MonadIO m) => [a] -> ExceptT T.Text m a
checkNum :: (MonadIO m) => T.Text -> ExceptT T.Text m Int
则checkFoo
可以写成如下,其中runExcept
前面没有liftIO
。我还在get
和put
之前删除了lift
。它们不是必需的,因为get
和put
会自动将自己提升到最近的包含StateT
的变压器。
checkFoo :: (MonadState Int m, MonadIO m) => [T.Text] -> m ()
checkFoo strs = runExceptT (check1 strs >>= checkNum >>= doFoo) >>= result
where
-- doFoo :: (MonadState Int m, MonadIO m) => Int -> m ()
doFoo n = do
old <- get
let s = old + n
liftIO $ putStrLn $ "The sum of your numbers: " <> show s
put s
pure ()
此版本在monadExceptT T.Text m
中运行check1 strs >>= checkNum >>= doFoo
管道,其中m
与checkFoo
类型签名中出现的monad(MonadState Int m, MonadIO m) => m
相同。对于同一个单子m
,结果再次传递给result :: (MonadIO m) => Either T.Text () -> m ()
。在您的代码中,checkFoo
是在m ~ StateT Int IO
处调用的,它满足MonadState Int m
和MonadIO m
的约束,所以类型检查器一切正常。
修改后的完整示例:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad.State.Strict
import Control.Monad.Except
import qualified Data.Text as T
import qualified Data.Text.Read as T
import qualified Data.Text.IO as T
main :: IO ()
main = do
_ <- execStateT loop 0
return ()
loop :: StateT Int IO ()
loop = do
liftIO $ putStrLn "Enter a Number"
line <- liftIO $ T.getLine
let ts = T.words line
checkFoo ts
loop
checkFoo :: (MonadState Int m, MonadIO m) => [T.Text] -> m ()
checkFoo strs = runExceptT (check1 strs >>= checkNum >>= doFoo) >>= result
where
-- doFoo :: (MonadState Int m, MonadIO m) => Int -> m ()
doFoo n = do
old <- get
let s = old + n
liftIO $ putStrLn $ "The sum of your numbers: " <> show s
put s
pure ()
check1 :: (MonadIO m) => [a] -> ExceptT T.Text m a
check1 ts =
if length ts == 1
then return $ head ts
else throwError "1 number please"
checkNum :: (MonadIO m) => T.Text -> ExceptT T.Text m Int
checkNum t = case T.decimal t of
Left _ -> throwError "input isn't a number"
Right (d, _) -> return $ d
result :: (MonadState Int m, MonadIO m) => Either T.Text () -> m ()
result (Left e) = liftIO $ T.putStrLn e
result (Right _) = return ()
这里的问题是check1
和checkNum
的类型注释。他们没有提到State Int
单子。它们应该是:
check1 :: [a] -> ExceptT T.Text (StateT Int IO) a
checkNum :: T.Text -> ExceptT T.Text (StateT Int IO) Int
那么checkFoo
的代码应该是:
checkFoo strs = (runExceptT
(check1 strs >>= checkNum >>= doFoo)) >>= result