5

我正在尝试在我正在处理的 Haskell 项目中使用“引文解析”包,但在实际代码中使用 EitherT 时遇到了麻烦。我知道它们是单子变换器,我想我理解这意味着什么,但是我似乎无法真正弄清楚如何使用它们。代表我正在尝试做的玩具示例如下:

module Main where
import Text.EditDistance
import Text.CSL.Input.Identifier
import Text.CSL.Reference
import Control.Monad.Trans.Class 
import Control.Monad.Trans.Either 

main = do
    putStrLn "Resolving definition"
    let resRef = runEitherT $ resolveEither "doi:10.1145/2500365.2500595"
    case resRef of 
                Left e -> do 
                    putStrLn ("Got error: "++ e)
                Right ref -> do
                    putStrLn ("Added reference to database: "++ (show ref))

在这里,resolveEither有类型:

resolveEither :: (HasDatabase s,
                  Control.Monad.IO.Class.MonadIO m,
                  mtl-2.1.3.1:Control.Monad.State.Class.MonadState s m)
                   => String -> EitherT String m Reference

runEitherT $ resolveEither "ref"具有以下类型:

runEitherT $ resolveEither "ref"
   :: (HasDatabase s,
       Control.Monad.IO.Class.MonadIO m,
       mtl-2.1.3.1:Control.Monad.State.Class.MonadState s m)
         => m (Either String Reference)

但是,这会产生以下错误:

Main.hs:10:34:
    No instance for (Control.Monad.IO.Class.MonadIO (Either [Char]))
      arising from a use of ‘resolveEither’
    In the first argument of ‘runEitherT’, namely
      ‘(resolveEither "doi:10.1145/2500365.2500595")’
    In the expression:
      runEitherT (resolveEither "doi:10.1145/2500365.2500595")
    In an equation for ‘resRef’:
        resRef = runEitherT (resolveEither "doi:10.1145/2500365.2500595")

我不知道如何解决或解决。

任何帮助将不胜感激,特别是从使用角度而不是实现角度处理 monad 转换器的教程的指针。

编辑:

为了反映 dfeuer 和 Christian 对答案的评论,如果我将 main 更改为以下内容,我仍然会遇到错误:

main = do
    putStrLn "Resolving definition"
    resRef <- runEitherT (resolveEither "doi:10.1145/2500365.2500595")
    case resRef of 
                Left e -> do 
                    putStrLn ("Got error: "++ e)
                Right ref -> do
                    putStrLn ("Added reference to database: "++ (show ref))

我现在得到的错误是:

No instance for (MonadState s0 IO)
  arising from a use of ‘resolveEither’
In the first argument of ‘runEitherT’, namely
  ‘(resolveEither "doi:10.1145/2500365.2500595")’
In a stmt of a 'do' block:
  resRef <- runEitherT (resolveEither "doi:10.1145/2500365.2500595")
In the expression:
  do { putStrLn "Resolving definition";
       resRef <- runEitherT (resolveEither "doi:10.1145/2500365.2500595");
       case resRef of {
         Left e -> do { ... }
         Right ref -> do { ... } } }

我正在编辑我的问题以及发表评论,因为这里的代码格式比在评论中要容易得多。

4

3 回答 3

7

我相信问题是你试图在你可能想要做的事情是执行它并在结果上进行模式匹配时进行模式匹配resRef

所以你应该试试这个:

main = do
    putStrLn "Resolving definition"
    resRef <- runEitherT $ resolveEither "doi:10.1145/2500365.2500595"
    case resRef of 
                Left e -> do
于 2014-09-30T00:47:03.867 回答
1

好的,所以我想我已经为我的原始问题找到了一个解决方案,即IO (Either String Reference)从函数中获取类型的值resolveEither(它为resolveDef它提供的函数所做的)。

所以,resolveEither返回一个类型

(HasDatabase s, MonadIO m, MonadState s m) => String -> EitherT String m Reference 

我们可以将其转换为一种类型

(HasDatabase s, MonadIO m, MonadState s m) => String -> m (Either String Reference)

使用runEitherT . resolveEither. 当我问这个问题时,这就是我站起来的地方。从那里,我尝试查看源代码以了解库如何从函数中提取Reference类型resolveEither。该库使用以下功能:

resolve :: (MonadIO m, MonadState s m, HasDatabase s) => String -> m Reference
resolve = liftM (either (const emptyReference) id) . runEitherT . resolveEither

但是,我们希望保留其中一个,即删除liftM (either (const emptyReference) id)

然而,这让我们回到了我们开始的地方,所以我再次查看了源代码,并弄清楚了如何使用这个函数。在库中,该函数在以下内容中使用,它将输出类型resolve从 type 的值转换为 type(MonadIO m, MonadState s m, HasDatabase s) => m Reference之一IO Reference

resolveDef :: String -> IO Reference
resolveDef url = do
  fn <- getDataFileName "default.db"
  let go = withDatabaseFile fn $ resolve url
  State.evalStateT go (def :: Database)

我们可以替换resolve前面的 withrunEitherT.resolveEither来获得一个返回 a 的函数IO (Either String Reference)

retEither s = do
    fn <- getDataFileName "default.db"
    let go = withDatabaseFile fn $ ( (runEitherT.resolveEither) s)
    State.evalStateT go (Database Map.empty)

(我已替换(def :: Database)(Database Map.empty)def 仅在内部定义citation-resolve

那么整体解决方案就变成了:

module Main where
import Text.EditDistance
import Text.CSL.Input.Identifier.Internal  
import Text.CSL.Input.Identifier
import Text.CSL.Reference
import Control.Monad.Trans.Either
import Control.Monad.State as State
import qualified Data.Map.Strict as Map

main = do
    putStrLn "Resolving definition"
    resRef <- retEither "doi:10.1145/2500365.2500595" 
    case resRef of 
                Left e -> putStrLn ("Got error: "++ e)
                Right ref -> putStrLn ("Added reference to database: "++ (show ref))

retEither s = do
    fn <- getDataFileName "default.db"
    let go = withDatabaseFile fn $ ((runEitherT.resolveEither) s)
    State.evalStateT go (Database Map.empty)

解决了原来的问题!

然而,任何关于风格的指针,或简化整个过程的方法都将非常感激。

于 2014-09-30T13:07:34.677 回答
1

您遇到了mtl基于类的方法的缺点之一:令人生畏的类型错误。transformers我认为想象一下基于 normal 的 monad 转换器会是什么样子会很有帮助。我希望这也能帮助你在一般情况下使用 monad 转换器。(顺便说一句,您似乎已经了解了大部分内容;我只是把它拼出来。)

给出类型是一个很好的开始方式。这是你所拥有的:

resolveEither :: (HasDatabase s,
                  MonadIO m,
                  MonadState s m)
                   => String -> EitherT String m Reference

约束中隐藏了一种类型,s,它稍后会回来咬你。粗略地说,这些约束表达了以下内容:s有一个数据库(无论在上下文中意味着什么);monad 或 monad 堆栈位于其底部,monad 堆栈中的某处m是一层。满足这些属性的最简单的 monad 堆栈是. 所以我们可以这样写:IOmStateT smHasDatabase s => StateT s IO

resolveEither' :: HasDatabase s
                  => String -> EitherT String (StateT s IO) Reference
resolveEither' = resolveEither

我们所做的只是指定类型,m因此它不再是变量。只要我们满足类约束,我们就不需要这样做。

现在更清楚的是有两层单子变换器。由于我们的 main 函数在IOmonad 中,我们希望最终得到一个 type 的值IO,我们可以“运行”它,例如使用<-indo表示法。我认为它是“剥离”单子变压器的层,从外到内。(这就是“使用”单子变压器的归结。)

对于EitherT,有一个函数runEitherT :: EitherT e m a -> m (Either e a)。看看如何m从“内”EitherT到“外”?对我来说,这是批判性的直观观察。同样对于StateT,有runStateT :: StateT s m a -> s -> m (a, s)

(顺便说一句,两者都被定义为记录访问器,这是惯用的,但会导致它们在 Haddock 中出现有点奇怪并且带有“错误”类型签名;我花了一段时间来学习查看 Haddock 的“构造函数”部分并在脑海中将EitherT e m a ->等添加到签名的前面。)

所以这增加了一个通用的解决方案,你基本上已经解决了:我们需要一个适当的 type 值s(我将称之为s),然后我们可以使用flip runStateT s . runEitherT $ resolveEither "ref"which has type IO ((Either String Reference), s)。(假设我已经将类型直接记在脑海中,但我可能没有。我flip第一次忘记了。)然后我们可以进行模式匹配或使用fst来获取Either,这似乎是您真正想要的。

如果您希望我解释 GHC 给您的错误,我会很高兴。非正式地,它是说你没有“运行”或剥离所有的单子变压器。更准确地说,它在观察那IO不是什么StateT s IO。通过使用runStateTand runEitherT,您可以强制或约束类型,以便最终满足类约束。当你把事情弄错时,这有点令人困惑。

哦,关于编写解决方案的惯用方式:我不确定单独的retEither函数在这里是否惯用,因为它看起来像是在干预全局状态,即打开某种数据库文件。这取决于图书馆的习语是什么样的。

此外,通过使用evalStateT,您会在评估后隐式丢弃状态,这可能是也可能不是一个坏主意。图书馆是否希望您重用数据库连接?

最后,您有一些额外的括号和一些缺少的类型签名;hlint会帮助你的。

于 2014-09-30T19:31:50.377 回答