Могу ли я использовать 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).