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

Код выше кажется действительно раздутым и противным. Что я могу сделать, чтобы сделать это проще?

4 ответа

Решение

Вы по существу хотите комбинацию Either e монада и тому IO монада. Вот для чего нужны монадные трансформаторы!

В этом случае вы можете использовать ErrorT монадный преобразователь, который добавляет обработку ошибок, используя 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
            -- [...]

К монадным трансформаторам можно привыкнуть, но они очень полезны.

Прежде чем делать стилистический рефакторинг, давайте сделаем шаг назад и подумаем о семантике того, что ваш код делает здесь.

У вас есть IO действие, которое производит что-то типа Either String PNG.PNGImage, где Left case - это сообщение об ошибке. Вы думаете, хотите сделать что-то с 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Вы могли бы сделать больше.

Используйте экземпляр 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.

Как насчет этого?

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 заклинание.)

Другие вопросы по тегам