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
The code above seems really bloated and nasty. What can I do to make it simpler?
You essentially want a combination of the Either e
monad and the IO
monad. That's what monad transformers are for!
In this case, you can use the ErrorT
monad transformer which adds error handling using Either
to an underlying monad, in this case 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
-- [...]
This keeps the old interface, although it would probably be even nicer to use ErrorT
for your function as well, and have the call to runErrorT
in your main
function.
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 transformers can take some getting used to, but they are very useful.
Before doing stylistic refactoring, let's take a step back and think about the semantics of what your code is doing here.
You've got an IO
action that produces something of type Either String PNG.PNGImage
, where the Left
case is an error message. You think want to do something with the Right
case when it exists, while leaving the error message as is. Think about what this composite operation might look like, if you condensed it into a single, generalized combinator:
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
While that could be useful as is, you may already have noticed that its type signature looks suspiciously similar to (>>=) :: (Monad m) => m a -> (a -> m b) -> m b
. In fact, if we generalize one step further by allowing the function to produce errors as well, we have exactly the type of (>>=)
where m a
becomes IO (Either String a)
. Unfortunately, you can't make that a Monad
instance, because you can't just glue type constructors together directly.
What you can do is wrap it in a newtype alias, and in fact it turns out that someone already has: this is just Either
used as a monad transformer, so we want ErrorT String IO
. Rewriting your function to use that gives this:
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
Now that we've merged the conceptual composite operation, we can begin condensing the specific operations more effectively. Collapsing the do
block into monadic function application is a good start:
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
And depending on what you're doing in oglLoadImg
, you might be able to do more.
Use an instance of Data.Traversable.Traversable
for Either
and then mapM
. An instance could be:
instance Traversable (Either a) where
sequenceA (Left x) = pure $ Left x
sequenceA (Right x) = Right <$> x
Now you can simply use 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.
How about this?
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
(I'm not totally happy with the either (return . Left)
bit and wonder whether it can be replaced with some sort of lift
incantation.)
精彩评论