在Haskell程序中使用返回的EitherT

5

我正在进行一个Haskell项目,尝试使用“citation-resolve”包,但是在实际代码中使用EitherT的过程中遇到了麻烦。我知道它们是Monad Transformer,也理解了这意味着什么,但似乎无法真正弄清如何使用它们。下面是一个玩具示例,代表我想要做的事情:

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")

我不知道如何解决或绕过这个问题。

任何帮助都将不胜感激,尤其是指向从使用角度而非实现角度处理单子变换器的教程的指针。

编辑:

为了反映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 { ... } } }

我正在编辑我的问题并添加评论,因为这里的代码格式化要比评论中容易得多。请注意,我保留了HTML标签。

为了重述 dfeuer 的答案,你可以尝试将 let resRef = runEitherT {-...-} 替换为 resRef <- runEitherT {-...-}。如果这样修复了问题而你还是不理解的话,我可以尝试解释一下。 - Christian Conkle
正如我的编辑所反映的那样,我已经尝试过了,但仍然出现了类型错误。我认为我知道为什么应该这样做,因为它会将单子(即IO)的一层“解开”,对吗? - AdamHarries
3个回答

7
我认为问题在于您正在尝试在 "resRef" 上进行“模式匹配”,而您可能想要执行它并在结果上进行模式匹配。因此,您应该尝试这样做:
```javascript const result = resRef(); // 在 result 上进行模式匹配 ```
main = do
    putStrLn "Resolving definition"
    resRef <- runEitherT $ resolveEither "doi:10.1145/2500365.2500595"
    case resRef of 
                Left e -> do

由于某些原因,仍然出现类似的错误:如果我修改 Main 为:main = do resRef <- runEitherT (resolveEither "...") case resRef of Left e -> do 我会得到以下错误: No instance for (MonadState s0 IO) arising from a use of ‘resolveEither’ In the first argument of ‘runEitherT’, namely ‘(resolveEither "...")’ In a stmt of a 'do' block: resRef <- runEitherT (resolveEither "...") In the expression: do { resRef <- runEitherT (resolveEither "..."); case resRef of { Left e -> do { ... } - AdamHarries

1
好的,我认为我已经找到了解决原问题的方法,即从函数resolveEither中获取类型为IO(Either String Reference)的值(它为提供的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。这是我提问时的进展。从那里开始,我尝试查看源代码,以了解库如何从函数 resolveEither 中提取 Reference 类型。该库使用以下函数:

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 的输出类型从 (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)

我们可以用runEitherT.resolveEither替换先前的resolve,以获得返回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),因为citation-resolve中只定义了内部的def

整体解决方案如下:

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)

这解决了最初的问题!

然而,任何关于风格或简化整个过程的指导都将不胜感激。


关于风格:我会添加类型签名(至少对于 retEither)。另外,withDatabaseFile fn $ ((runEitherT.resolveEither) s) 等同于 withDatabaseFile fn . runEitherT . resolveEither $ s - chi
啊,当然 - 我忘记了类型签名,因为我只是试图让一些东西工作,而且更少括号的版本看起来也不错。谢谢! - AdamHarries

1
您遇到了“mtl”类别方法的一个缺点:令人生畏的类型错误。我认为想象一下使用普通的基于“transformers”的单子变换器的情况会很有帮助。我希望这也能帮助您对单子变换器有更好的理解。顺便说一句,看起来您已经理解了大部分内容,我只是在详细解释一下。给出类型是一个很好的开始。这是您之前的类型:
resolveEither :: (HasDatabase s,
                  MonadIO m,
                  MonadState s m)
                   => String -> EitherT String m Reference

约束中隐藏了一种类型s,稍后可能会对你造成影响。粗略地说,这些约束表达了以下内容:s具有数据库(在上下文中意味着什么);单子或单子堆栈mIO为基础,并且在单子堆栈m的某个地方存在一个StateT s层。满足这些属性的最简单的单子堆栈m将是HasDatabase s => StateT s IO。因此,我们可以写出以下内容:

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

我们所做的就是指定m的类型,使其不再是一个变量。只要满足类约束,我们不需要这样做。
现在更清晰地看到了两层单子变换器。由于我们的主函数在IO单子中,我们希望最终得到一个IO类型的值,我们可以使用<-do表示法中“运行”。我认为它是从外到内“剥离”单子变换器层的过程。(这就是使用单子变换器的本质。)
对于EitherT,有一个函数runEitherT :: EitherT e m a -> m (Either e a)。注意m是如何从EitherT的“内部”移动到“外部”的。对我来说,这是关键的直觉观察。同样对于StateT,有runStateT :: StateT s m a -> s -> m (a, s)
(顺带一提,这两个都被定义为记录访问器,这是惯用法,但会导致它们在Haddock中显示得有些奇怪,并且具有“错误”的类型签名;我花了一些时间学习如何在Haddocks的“构造函数”部分查找并在签名前面添加EitherT e m a ->等。)

因此,这就加起来形成了一个通用解决方案,你已经基本上解决了:我们需要一个适当的s类型的值(我将其称为s),然后我们可以使用flip runStateT s . runEitherT $ resolveEither "ref",其类型为IO ((Either String Reference), s)。(假设我在我的脑海中保持了类型的正确性,我可能没有。我第一次忘记了flip.)然后我们可以模式匹配或使用fst来获得Either,这似乎是你真正想要的。)

如果你想让我解释GHC给出的错误,我很乐意。非正式地说,它是在说你没有“运行”或剥离所有单子变换器。更确切地说,它观察到IO不像StateT s IO这样的东西。通过使用runStateTrunEitherT,您可以强制或约束类型,以使类约束得到满足。当您稍微出错时,这有点令人困惑。
哦,关于编写解决方案的惯用方式:我不确定在这里单独使用retEither函数是否符合惯例,因为它看起来正在干扰全局状态,即打开某种数据库文件。这取决于库的惯用方式。
另外,通过使用evalStateT,您隐含地丢弃了评估后的状态,这可能是好事或坏事。库是否希望您重用数据库连接?
最后,您有一些额外的括号和一些缺少的类型签名;hlint将帮助您解决这些问题。

啊,这正是我需要知道的!谢谢!我想我开始理解 runEitherT 如何将一个 monad 移到 EitherT 之外,但我没有意识到还有另一个 monad 需要被剥离。我已将您的答案标记为已接受,因为我认为虽然我的代码(或多或少)能够工作,但你的解释更好地说明了如何处理像这样的代码。 - AdamHarries

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接