在进行风格重构之前,让我们退一步并思考一下您的代码在语义上做了什么。
您拥有一个生成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
中的具体操作,你可能还能做更多的事情。
return (PNG.loadPNGFile filename)
代替ErrorT (PNG.loadPNGFile filename)
吗? - Zhen