Могу ли я использовать bind/fmap здесь
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
Код выше кажется действительно раздутым и неприятным. Что я могу сделать, чтобы сделать его проще?
Ответы
Ответ 1
Вам нужна комбинация монады Either e
и монады IO
. То, что монадные трансформаторы для!
В этом случае вы можете использовать ErrorT
monad transformer, который добавляет обработку ошибок с помощью Either
в базовую монаду, в данном случае 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
-- [...]
Модальные трансформаторы могут привыкнуть, но они очень полезны.
Ответ 2
Прежде чем делать стилистический рефакторинг, сделайте шаг назад и подумайте о семантике того, что делает ваш код здесь.
У вас есть действие 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
, используемый в качестве монадного трансформатора, поэтому мы хотим 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
, вы можете сделать больше.
Ответ 3
Используйте экземпляр 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.
Ответ 4
Как насчет этого?
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
).