我能否在这里使用bind/fmap?

6
loadTexture :: String -> IO (Either String GL.GLuint)
loadTexture filename = do
    p <- PNG.loadPNGFile filename
    oglLoadImg p
    where
        oglLoadImg :: (Either String PNG.PNGImage) -> IO (Either String GL.GLuint)
        oglLoadImg (Left e) = return $ Left e 
        oglLoadImg (Right png) = do
            ... I need todo IO stuff in here

上面的代码看起来非常臃肿且不易理解。我该如何简化它?

4个回答

14

你需要的是将Either e monad和IO monad结合起来。这就是Monad transformers的用途!

在本例中,您可以使用ErrorT monad transformer,它将错误处理与Either添加到底层monad(在本例中为IO)中。

import Control.Monad.Error

loadTexture :: String -> IO (Either String GL.GLuint)
loadTexture filename = runErrorT $ ErrorT (PNG.loadPNGFile filename) >>= oglLoadImg
    where
        oglLoadImg :: PNG.PNGImage -> ErrorT String IO GL.GLuint
        oglLoadImg png = do
            -- [...]

这将保留旧界面,尽管对于您的函数来说,使用ErrorT可能会更好,并在您的main函数中调用runErrorT

loadTexture :: String -> ErrorT String IO GL.GLuint
loadTexture filename = ErrorT (PNG.loadPNGFile filename) >>= oglLoadImg
    where
        oglLoadImg :: PNG.PNGImage -> ErrorT String IO GL.GLuint
        oglLoadImg png = do
            -- [...]

单子变换器可能需要一些时间适应,但它们非常有用。


可以使用return (PNG.loadPNGFile filename)代替ErrorT (PNG.loadPNGFile filename)吗? - Zhen

12

在进行风格重构之前,让我们退一步并思考一下您的代码在语义上做了什么。

您拥有一个生成Either String PNG.PNGImage类型的东西的IO操作,其中Left情况是错误消息。 您想在存在Right情况时执行某些操作,同时保留错误消息。 如果将其缩成单个通用组合器,则此复合操作可能看起来像什么:

doIOWithError :: IO (Either String a) -> (a -> IO b) -> IO (Either String b)
doIOWithError x f = do x' <- x
                       case x' of
                           Left err -> return (Left err)
                           Right y  -> f y

虽然它本身就很有用,但您可能已经注意到,它的类型签名看起来非常类似于(>>=) :: (Monad m) => m a -> (a -> m b) -> m b。实际上,如果我们进一步推广,允许该函数产生错误,那么我们会得到恰好与 (>>=) 相同类型的函数,其中m a变成了IO(Either String a)。不幸的是,你不能将其作为Monad 实例,因为你不能直接将类型构造器粘合在一起。

您可以将其包装在一个新类型别名中,实际上事实证明,有人已经这样做了:这只是将Either用作单子变换器,因此我们想要的是ErrorT String IO。将您的函数重写以使用它将如下所示:

loadTexture :: String -> ErrorT String IO GL.GLuint
loadTexture filename = do
    p <- ErrorT $ loadPNGFile filename
    lift $ oglLoadImg p
    where
        oglLoadImg :: PNG.PNGImage -> IO GL.GLuint
        oglLoadImg png = do putStrLn "...I need todo IO stuff in here"
                            return 0

既然我们已经合并了概念上的组合操作,我们可以更有效地开始压缩具体操作。将do块折叠成单子函数应用是一个不错的开始:

loadTexture :: String -> ErrorT String IO GL.GLuint
loadTexture filename = lift . oglLoadImg =<< ErrorT (loadPNGFile filename)
    where
        oglLoadImg :: PNG.PNGImage -> IO GL.GLuint
        oglLoadImg png = do putStrLn "...I need todo IO stuff in here"
                            return 0

而且,根据你在 oglLoadImg 中的具体操作,你可能还能做更多的事情。


4

使用Data.Traversable.Traversable的一个实例来处理Either,然后使用mapM函数。这是一个可能的实现:

instance Traversable (Either a) where
  sequenceA (Left x)  = pure $ Left x
  sequenceA (Right x) = Right <$> x

现在您可以直接使用forM
loadTexture :: String -> IO (Either String GL.GLuint)
loadTexture filename = do
  p <- PNG.loadPNGFile filename
  forM p $ \p -> do
    -- Whatever needs to be done
  -- continue here.

2
这个怎么样?
loadTexture :: String -> IO (Either String GL.GLuint)
loadTexture filename = either (return . Left) oglLoadImg =<< PNG.loadPNGFile filename
    where
        oglLoadImg :: PNG.PNGImage -> IO (Either String GL.GLuint)
        oglLoadImg png = do -- IO stuff in here

我对 either (return . Left) 这一部分并不完全满意,想知道是否可以用某种 lift 的方式来替代它。

是的,lift 在这里是合适的。请参见 hammar 和我几乎同时回答的演示 ErrorT 的细节。:] - C. A. McCann
@tm1rbrt 我搞砸了函数的类型签名,希望现在已经修复了。但是 hammer 和 C.A.McCann 的答案更好。 - dave4420

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