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