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
上面的代码看起来真的很臃肿和讨厌。我该怎么做才能让它变得更简单?
答案 0 :(得分:14)
你基本上想要Either e
monad和IO
monad的组合。这就是monad transformers的用途!
在这种情况下,您可以使用the 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
也可能更好,并在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
咒语代替。)