我可以在这里使用bind / fmap吗?

时间:2011-08-23 20:32:03

标签: haskell

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 个答案:

答案 0 :(得分:14)

你基本上想要Either e monad和IO monad的组合。这就是monad transformers的用途!

在这种情况下,您可以使用the ErrorT monad transformerEither的错误处理添加到基础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也可能更好,并在runErrorT函数中调用main

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
            -- [...]

Monad变形金刚可以接受一些习惯,但它们非常有用。

答案 1 :(得分:12)

在进行风格重构之前,让我们退后一步,考虑一下代码在这里所做的语义。

您有一个IO操作产生Either String PNG.PNGImage类型的内容,其中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实例,因为你不能直接将类型构造函数粘合在一起。

你可以做的是将它包装在一个newtype别名中,事实证明有人已经拥有:这只是Either用作monad变换器,所以我们想要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块折叠到monadic函数应用程序中是一个好的开始:

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中所做的事情,您可以做更多事情。

答案 2 :(得分: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.

答案 3 :(得分: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咒语代替。)