6 Stimmen

Könnte ich hier ein bind/fmap verwenden?

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
            ... Ich muss hier IO-Aktionen ausführen

Der obige Code scheint wirklich aufgebläht und unangenehm. Was kann ich tun, um ihn einfacher zu gestalten?

14voto

hammar Punkte 136080

Du möchtest im Wesentlichen eine Kombination des Either e Monads und des IO Monads. Dafür sind Monaden-Transformer da!

In diesem Fall kannst du den ErrorT Monaden-Transformer verwenden, der die Fehlerbehandlung mit Either zu einem zugrunde liegenden Monaden hinzufügt, in diesem Fall 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
            -- [...]

Dies behält die alte Schnittstelle bei, obwohl es wahrscheinlich noch besser wäre, ErrorT auch für deine Funktion zu verwenden und den Aufruf von runErrorT in deiner main Funktion zu haben.

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
            -- [...]

Monaden-Transformer können etwas gewöhnungsbedürftig sein, aber sie sind sehr nützlich.

12voto

C. A. McCann Punkte 76279

Vor dem Durchführen von stilistischem Refactoring sollten wir einen Schritt zurücktreten und über die Semantik nachdenken, was dein Code hier tut.

Du hast eine IO-Aktion, die etwas vom Typ Either String PNG.PNGImage produziert, wobei der Left-Fall eine Fehlermeldung ist. Du möchtest etwas mit dem Right-Fall machen, wenn er existiert, während die Fehlermeldung erhalten bleibt. Überlege, wie diese zusammengesetzte Operation aussehen könnte, wenn du sie in einen einzigen, verallgemeinerten Kombinator zusammenfassen würdest:

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

Obwohl das so nützlich sein könnte, hast du vielleicht bereits festgestellt, dass die Typsignatur verdächtig ähnlich aussieht wie (>>=) :: (Monad m) => m a -> (a -> m b) -> m b. Tatsächlich haben wir, wenn wir einen Schritt weiter verallgemeinern, indem wir die Funktion auch Fehler produzieren lassen, genau den Typ von (>>=), bei dem m a zu IO (Either String a)Monad-Instanz machen, weil du Typkonstruktoren nicht direkt zusammenkleben kannst.

Was du machen kannst, ist es in ein neues Typalias zu verpacken, und tatsächlich hat das schon jemand getan: das ist einfach Either als Monaden-Transformer verwendet, also wollen wir ErrorT String IO. Wenn du deine Funktion so umschreibst, erhältst du das hier:

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 "...Hier muss ich IO-Zeug machen"
                            return 0

Nachdem wir die konzeptuelle zusammengesetzte Operation zusammengeführt haben, können wir beginnen, die spezifischen Operationen effektiver zu kondensieren. Das Zusammenführen des do-Blocks in monadische Funktionsanwendung ist ein guter Anfang:

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 "...Hier muss ich noch IO-Zeug machen"
                            return 0

Und je nachdem, was du in oglLoadImg machst, könntest du noch mehr tun.

4voto

fuz Punkte 82245

Verwenden Sie eine Instanz von Data.Traversable.Traversable für Either und dann mapM. Eine Instanz könnte sein:

instance Traversable (Either a) where
  sequenceA (Left x)  = pure $ Left x
  sequenceA (Right x) = Right <$> x

Jetzt können Sie einfach forM verwenden:

loadTexture :: String -> IO (Either String GL.GLuint)
loadTexture filename = do
  p <- PNG.loadPNGFile filename
  forM p $ \p -> do
    -- Was auch immer getan werden muss
  -- Hier weitermachen.

2voto

dave4420 Punkte 45576

Wie wäre es damit?

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

(Ich bin nicht ganz zufrieden mit dem either (return . Left) Teil und frage mich, ob es durch irgendeine Art von lift Beschwörung ersetzt werden kann.)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X