【问题标题】:Using returned EitherT in haskell program在 haskell 程序中使用返回的 EitherT
【发布时间】:2014-11-24 11:30:11
【问题描述】:

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

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 { ... } } }

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

【问题讨论】:

  • 改写 dfeuer 的回答,您可以尝试用 resRef &lt;- runEitherT {-...-} 替换 let resRef = runEitherT {-...-} 吗?如果这样解决了它并且您不理解,我可以尝试解释。
  • 正如我的编辑所反映的,我已经尝试过了,但仍然出现类型错误。我认为我知道为什么它应该起作用,因为它“解开”了一层monad(即IO),对吗?

标签: haskell monads monad-transformers either io-monad


【解决方案1】:

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

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

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

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

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

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

现在更清楚的是有两层单子转换器。由于我们的主要函数在IO monad 中,我们希望得到一个IO 类型的值,我们可以“运行”它,例如在do 表示法中使用&lt;-。我认为它是从外到内“剥离”单子变压器的层。(这就是“使用”单子变压器的归结。)

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

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

所以这加起来了一个通用的解决方案,你基本上已经解决了:我们需要一个合适的 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 移到EtherT 之外,但我没有意识到还有另一个 monad 也需要剥离。我已将您的答案标记为已接受,因为我认为尽管我的答案(或多或少)提供了工作代码,但您的答案提供了一个更好的解释,说明了如何处理这样的代码。
【解决方案2】:

好的,所以我想我已经为我的原始问题找到了解决方案,即从函数 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) =&gt; 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替换为runEitherT.resolveEither,得到一个返回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)

解决了原来的问题!

非常感谢任何关于风格的指针,或简化整个过程的方法。

【讨论】:

  • 关于风格:我会添加类型签名(至少到retEither)。此外,withDatabaseFile fn $ ((runEitherT.resolveEither) s) 等价于 withDatabaseFile fn . runEitherT . resolveEither $ s
  • 啊,当然 - 我忘记了类型签名,因为我只是想让一些东西正常工作,而且更反对括号的版本看起来也不错。谢谢!
【解决方案3】:

我认为问题在于您尝试在resRef 上进行模式匹配,而您可能想要做的是执行它并在结果上进行模式匹配.

所以你应该试试这个:

main = do
    putStrLn "Resolving definition"
    resRef <- runEitherT $ resolveEither "doi:10.1145/2500365.2500595"
    case resRef of 
                Left e -> do

【讨论】:

  • 由于某些原因仍然会出现类似的错误:如果我将 Main 修改为:main = do resRef &lt;- runEitherT (resolveEither "...") case resRef of Left e -&gt; do 我收到错误:`由于使用 'resolveEither' 而导致 (MonadState s0 IO) 没有实例在 'runEitherT' 的第一个参数中,即 '(resolveEither "...")' 在 'do' 块的 stmt 中: resRef do { ... }` 的 case resRef
猜你喜欢
  • 2013-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-10
  • 2012-06-03
  • 2023-04-07
  • 1970-01-01
相关资源
最近更新 更多