将StateT移入和移出IO

4

我相信我一定错过了什么。

我是Haskell的新手,学习曲线非常陡峭。在我的玩具项目中,我到达了一个点,我真的想使用State monad来避免在各个地方传递一千个参数。我不知道如何将这个State monad从IO传递到纯代码中。类似于以下概念(除了ExceptT之外使用StateT):

import Control.Monad.Except
import Control.Monad.Identity

type PlayM = Except String 
type PlayMIO = ExceptT String IO

puree :: String -> PlayM String
puree = return . ("bb"++)

impuree :: String -> PlayMIO String
impuree s = do
  a <- return $ runIdentity $ runExceptT $ puree s
  return $ "aa" ++ a

main = do
  runExceptT $ impuree "foo"
  putStrLn "hi"

但是这段代码无法编译,会出现类似以下的错误信息:

play.hs:15:20:
Couldn't match expected type ‘[Char]’
            with actual type ‘Either String String’
In the second argument of ‘(++)’, namely ‘a’
In the second argument of ‘($)’, namely ‘"aa" ++ a’

我现在明白了为什么这段代码无法编译以及为什么类型是它们所表示的那样,但我就是想不通怎么做。这似乎并不应该很难,但在Haskell中我的直觉经常是不准确的。

感谢您的帮助!

-g

2个回答

5
你接近了答案。让我们跟随类型上的类型洞(_):
impuree :: String -> PlayMIO String
impuree s = do
  a <- _ . runIdentity . runExceptT $ puree s
  return $ "aa" ++ a

这告诉我们需要一个类型:
Test.hs:15:8:
    Found hole ‘_’
      with type: m0 (Either String String) -> ExceptT String IO [Char]
    Where: ‘m0’ is an ambiguous type variable
    Relevant bindings include
      s :: String (bound at Test.hs:13:9)
      impuree :: String -> PlayMIO String (bound at Test.hs:13:1)
    In the first argument of ‘(.)’, namely ‘_’
    In the expression: _ . return . runIdentity . runExceptT
    In a stmt of a 'do' block:
      a <- _ . return . runIdentity . runExceptT $ puree s

现在,我们有了一种可以将 m (Either e b) 转换成 ExceptT e m b 的方法:

ExceptT :: m (Either e b) -> ExceptT e m b

应用这个方法,我们得到了正确答案:
impuree :: String -> PlayMIO String
impuree s = do
  a <- ExceptT . return . runIdentity . runExceptT $ puree s
  return $ "aa" ++ a

如果我们查看文档,可以看到模式ExceptT . f . runExceptT是通过函数进行抽象化的。
mapExceptT :: (m (Either e a) -> n (Either e' b)) -> ExceptT e m a -> ExceptT e' n b

在我们的情况下,mIdentity,而 nIO。使用这个,我们得到:
impuree :: String -> PlayMIO String
impuree s = do
  a <- mapExceptT (return . runIdentity) $ puree s
  return $ "aa" ++ a

抽象出模式

这可能在这里有些过于复杂,但值得注意的是,有一个名为mmorph的包,使得使用单子变形(从一个单子到另一个单子的转换)更加方便。该包具有函数generalize :: Monad m => Identity a -> m a,我们可以用它来:

impuree :: String -> PlayMIO String
impuree s = do
  a <- mapExceptT generalize $ puree s
  return $ "aa" ++ a

更进一步的概括

既然我们正在谈论 mmorph,我们不妨使用更一般化的形式:

impuree :: String -> PlayMIO String
impuree s = do
  a <- hoist generalize $ puree s
  return $ "aa" ++ a

hoist 通用化了mapExceptT的功能,适用于所有类似于单子变换器的东西,在其中你可以将一个单子态射应用到底层单子上:

hoist :: (MFunctor t, Monad m) => (forall a. m a -> n a) -> t m b -> t n b

在这里第一个正确答案之后的所有内容都是额外的,并且不需要理解它以理解和使用解决方案。但是在某些情况下可能会有用,这就是为什么我包含它的原因。识别单子态射的一般模式可以节省时间,但是您始终可以更明确地执行操作,而不需要额外的抽象层次。


太棒了。解释得非常好,还有额外的内容。谢谢。你不仅回答了我的问题,还帮助了我的学习。 - George Madrid
我非常确定 EitherT 是错误地出现了。 - dfeuer
1
@dfeuer 谢谢!我就感觉会这样。如果那是真正的名字就好了... - David Young
我们怎么会取一个不相符的名字呢? - dfeuer
EitherT - Cirdec

2

另一种方法是将您的pureeimpuree操作类型类多态化。这是通常的mtl方式:要求一些类,然后在顶层的某个地方选择一个具体的单子来实例化所有适当的类。因此:

import Control.Monad.Except
import Control.Monad.Identity

type PlayM = Except String 
type PlayMIO = ExceptT String IO

puree :: Monad m => String -> m String
puree = return . ("bb"++)

impuree :: Monad m => String -> m String
impuree s = do
  a <- puree s
  return $ "aa" ++ a

main = do
  runExceptT $ impuree "foo"
  putStrLn "hi"

在这个例子中,你的代码特别无聊,因为你没有使用任何 IOExceptT 的特殊功能。下面是如果你使用了它们的代码示例:
-- in particular, puree :: String -> PlayM String
puree :: MonadError String m => String -> m String
puree "heck" = throwError "Watch your language!"
puree s = return ("bb" ++ s)

-- in particular, impuree :: String -> PlayMIO String
impuree :: (MonadError String m, MonadIO m) => String -> m String
impuree s = do
  s' <- puree s
  liftIO . putStrLn $ "hah! what kind of input is " ++ s ++ "?!"
  return ("aa" ++ s)

main = do
  runExceptT (impuree "foo")
  putStrLn "hi"

谢谢。这也是一个很好的回答,对我来说很有教育意义。看起来我需要FlexibleContexts才能将“MonadError String m”放入puree的上下文中?这样做有什么不利之处吗? - George Madrid
@GeorgeMadrid 实际上,没有任何不利影响。假设情况下,这将排除使用不支持该扩展的编译器来使用您的代码;由于GHC在Haskell编译器领域是如此强大,除非您已经计划使用一种小众编译器,否则不必担心。也没有太多技术缺点:放宽该扩展的限制是为了使语言实现更容易,在真正知道类型类作为一种语言特性是否可行之前就已经采取了这些限制。根据我们现在所知,我认为会做出不同的选择。 - Daniel Wagner
我对函数式编程以及特别是Haskell的理解迅速变化感到着迷。任何你读过的书都已经过时了。我惊讶于Haskell类型推断引擎的强大,以及它不仅仅是一种便利。我正在学习它是使用Haskell的基本工具。再次感谢。 - George Madrid

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