2012-11-06 10 views
6

Perché ho semplificato troppo nel mio other question prima, vorrei dare un esempio più chiaro qui.Elegante caso di haskell/gestione degli errori nelle monadi sequenziali

Come posso gestire le situazioni in cui devo verificare le condizioni di certian in modo sequenziale senza annidare più casi? Con "modo sequenziale" intendo ottenere un valore (ad esempio da stdin), controllando questo valore per una determinata condizione e in base al risultato che ottiene un altro valore e così via.

Esempio:

sequen :: IO String 
sequen = do 
    a <- getLine 
    case a of 
    "hi" -> do 
     putStrLn "hello!" 
     b <- getLine 
     case b of 
     "how are you?" -> do 
      putStrLn "fine, thanks" 
      return "nice conversation" 
     _ -> return "error 2" 
    _ -> return "error 1" 

So che ci sono modi migliori per scrivere una chiacchierata bot, si deve solo dimostrare la natura sequenziale del problema. Come puoi vedere, con ogni caso annidato, il codice diventa anche più rientrato.

C'è un modo per strutturare meglio tale codice? Sto pensando di gestire gli "errori" in un punto e di descrivere il "percorso di successo" senza la gestione degli errori distribuita su di esso.

risposta

20

Naturalmente. Questo è esattamente ciò per cui è stato creato EitherT. È possibile scaricarlo dal Control.Monad.Trans.Either nel pacchetto eitherT.

import Control.Monad.Trans.Class 
import Control.Monad.Trans.Either 

main = do 
    e <- runEitherT $ do 
     a <- lift getLine 
     case a of 
      "hi" -> lift $ putStrLn "hello!" 
      _ -> left 1 
     b <- lift getLine 
     case b of 
      "how are you?" -> lift $ putStrLn "fine, thanks!" 
      _    -> left 2 
     return "nice conversation" 
    case e of 
     Left n -> putStrLn $ "Error - Code: " ++ show n 
     Right str -> putStrLn $ "Success - String: " ++ str 

EitherT interrompe il blocco di codice corrente ogni volta che incontra una dichiarazione left, e la gente di solito uso questo per indicare le condizioni di errore.

Il tipo di blocco interno è EitherT Int IO String. Quando lo si runEitherT, si ottiene IO (Either Int String). Il tipo Left corrisponde al caso in cui non è riuscito con un valore left e il valore Right significa che ha raggiunto correttamente la fine del blocco.

+0

Fantastico. Esattamente quello che volevo sapere. Grazie! –

+2

Sulla nota di EitherT, ho scritto un post su questo blog che è stato abbastanza ben accolto: http://ocharles.org.uk/blog/posts/2012-07-24-in-praise-of-EitherT.html – ocharles

+0

@ocharles ho letto che qualche tempo fa e ho pensato che dovrebbe essere la soluzione al mio problema, ma non ho potuto applicarlo fino ad ora. È possibile collegare a questa domanda nel proprio post :) –

0

Avviso: rispondente di un nuovo amico Haskell.

È possibile evitare questo tipo di scalinata con la monade Forse. Un buon esempio all'inizio di this chapter

Tuttavia, si vorrebbe qualcosa di simile con un E monadico (presumibilmente ce n'è uno) dal momento che si stanno restituendo i codici di errore.

L'idea di base è che una volta ottenuto l'errore "Sinistra 1" si interrompono eventuali passaggi futuri (a causa di una valutazione lenta).

+0

Ah, vedo che ero sulla strada giusta secondo @Gabriel di seguito. –

+0

A causa della valutazione lazy? O a causa della definizione dell'istanza monad per 'Either'? –

1

Dal momento che sono necessariamente nel IO Monade, si sta meglio con le funzionalità di gestione degli errori della IO della monade, invece di accatastamento una monade errore sulla parte superiore del IO. Evita tutto il pesante lift ing:

import Control.Monad (unless) 
import Control.Exception (catch) 
import Prelude hiding (catch) 
import System.IO.Error (ioeGetErrorString) 

main' = do 
    a <- getLine 
    unless (a == "hi") $ fail "error 1" 
    putStrLn "hello!" 
    b <- getLine 
    unless (b == "how are you?") $ fail "error 2" 
    putStrLn "fine, thanks" 
    return "nice conversation" 

main = catch main' $ return . ioeGetErrorString 

In questo caso, i vostri errori sono semplicemente String s, che vengono scagliati dal IO s' fail, come userError. Se si desidera lanciare un altro tipo, sarà necessario utilizzare throwIO anziché fail.

5

Ho scritto una serie di post un po 'indietro sui miei apprendimenti dei tipi EitherEitherT.Si può leggere qui: http://watchchrislearn.com/blog/2013/12/01/working-entirely-in-eithert/

utilizzare il pacchetto di errors per ottenere un po 'di belle aiutanti intorno usando EitherT (left e right funzioni, ad esempio, per tornare versioni sollevato di Left e Right).

Estrarre le potenziali condizioni di errore nei propri helper, è possibile rendere la linea principale del codice letto in modo totalmente sequenziale, senza istruzioni caso che controllano i risultati.

Da quel post, è possibile vedere come la sezione runEitherT è un blocco sequenziale di lavoro, solo che si verifica la meccanica di errore di EitherT. Ovviamente questo codice è abbastanza elaborato per mostrare come MaybeT riproduce anche all'interno di EitherT. Nel codice reale sarebbe solo la storia che volevi raccontare, con un singolo Left/Right alla fine.

import Control.Error 
import Control.Monad.Trans 

-- A type for my example functions to pass or fail on. 
data Flag = Pass | Error 

main :: IO() 
main = do 
    putStrLn "Starting to do work:" 

    result <- runEitherT $ do 
     lift $ putStrLn "Give me the first input please:" 
     initialText <- lift getLine 
     x <- eitherFailure Error initialText 

     lift $ putStrLn "Give me the second input please:" 
     secondText <- lift getLine 
     y <- eitherFailure Pass (secondText ++ x) 

     noteT ("Failed the Maybe: " ++ y) $ maybeFailure Pass y 

    case result of 
    Left val -> putStrLn $ "Work Result: Failed\n " ++ val 
    Right val -> putStrLn $ "Work Result: Passed\n " ++ val 

    putStrLn "Ok, finished. Have a nice day" 

eitherFailure :: Monad m => Flag -> String -> EitherT String m String 
eitherFailure Pass val = right $ "-> Passed " ++ val 
eitherFailure Error val = left $ "-> Failed " ++ val 

maybeFailure :: Monad m => Flag -> String -> MaybeT m String 
maybeFailure Pass val = just $ "-> Passed maybe " ++ val 
maybeFailure Error _ = nothing 
+0

Bel esempio! Grazie per aver ricordato che puoi mettere i casi in funzioni separate. Rende la funzione principale molto più piacevole da leggere. –