2010-01-26 17 views
6

¿Cuál es una buena manera para que una función Haskell verifique una serie de condiciones diferentes y devuelva un mensaje de error cuando falla?Haskell forma de hacer la comprobación de errores de varios elementos con aborto

En Python o un lenguaje similar, sería sencillo:

if failure_1: 
    return "test1 failed" 
if failure_2: 
    return "test2 failed" 
... 
if failure_n: 
    return "testn failed" 
do_computation 

¿Cómo se hace esto sin el caso arbitrariamente anidados/if en Haskell?

Edición: algunas de las condiciones de prueba pueden requerir IO, que pone los resultados de la prueba en la mónada IO. Creo que esto pone un torpe en una serie de soluciones.

Respuesta

12

Así que, usted Está atascado dentro de IO, y quiere verificar un montón de condiciones sin muchas anotaciones if s. Espero que me perdones una digresión sobre la resolución de problemas más generales en Haskell a modo de respuesta.

Considere en forma resumida cómo debe comportarse esto. Comprobación de una condición tiene uno de dos resultados:

  • éxito, en cuyo caso el programa se ejecuta el resto de la función
  • Si no, en cuyo caso el programa descarta el resto de la función y devuelve el mensaje de error.

La comprobación de múltiples condiciones se puede ver de forma recursiva; cada vez que ejecuta "el resto de la función", golpea la siguiente condición, hasta llegar al paso final que simplemente devuelve el resultado. Ahora, como primer paso para resolver el problema, desglosémoslo usando esa estructura, así que, básicamente, queremos convertir un montón de condiciones arbitrarias en partes que podamos componer juntas en una función multi-condicional. ¿Qué podemos concluir sobre la naturaleza de estas piezas?

1) Cada pieza puede devolver uno de dos tipos diferentes; un mensaje de error o el resultado del siguiente paso.

2) Cada pieza debe decidir si se ejecuta el siguiente paso, por lo tanto, al combinar los pasos, debemos asignarle la función que representa el siguiente paso como argumento.

3) Dado que cada pieza espera que se le dé el siguiente paso, para preservar la estructura uniforme necesitamos una manera de convertir el paso final, incondicional en algo que se ve igual a un paso condicional.

El primer requisito obviamente sugiere que querremos un tipo como Either String a para nuestros resultados. Ahora necesitamos una función de combinación para ajustarse al segundo requisito y una función de envoltura para el tercero. Además, al combinar los pasos, es posible que deseemos tener acceso a los datos de un paso anterior (por ejemplo, validar dos entradas diferentes y luego verificar si son iguales), por lo que cada paso deberá tomar el resultado del paso anterior como argumento.

Por lo tanto, llamando al tipo de cada paso err a como una forma abreviada, ¿qué tipos podrían tener las otras funciones?

combineSteps :: err a -> (a -> err b) -> err b 
wrapFinalStep :: a -> err a 

Ahora bien, esas firmas de tipos parecen extrañamente familiar, ¿verdad?

Esta estrategia general de "ejecutar un cálculo que puede fallar temprano con un mensaje de error" de hecho se presta a una implementación monádica; y de hecho, el mtl package ya tiene uno. Más importante aún para este caso, también tiene un transformador de mónada, lo que significa que puede agregar la estructura de mónada de error a otra mónada, como IO.

lo tanto, sólo puede importar el módulo, hacer un sinónimo de tipo para envolver IO en una cálida difusa ErrorT, y ya está:

import Control.Monad.Error 

type EIO a = ErrorT String IO a 

assert pred err = if pred then return() else throwError err 

askUser prompt = do 
    liftIO $ putStr prompt 
    liftIO getLine 

main :: IO (Either String()) 
main = runErrorT test 

test :: EIO() 
test = do 
    x1 <- askUser "Please enter anything but the number 5: " 
    assert (x1 /= "5") "Entered 5" 
    x2 <- askUser "Please enter a capital letter Z: " 
    assert (x2 == "Z") "Didn't enter Z" 
    x3 <- askUser "Please enter the same thing you entered for the first question: " 
    assert (x3 == x1) $ "Didn't enter " ++ x1 
    return() -- superfluous, here to make the final result more explicit 

El resultado de ejecutar test, como era de esperar, es Right() para el éxito, o Left String para el fracaso, donde el String es el mensaje apropiado; y si un assert devuelve una falla, no se realizará ninguna de las siguientes acciones.

Para probar el resultado de las acciones IO puede que le resulte más fácil de escribir una función de ayuda similar a assert que en vez toma un argumento de IO Bool, o algún otro método.

También tenga en cuenta el uso de liftIO para convertir IO acciones en valores en EIO y runErrorT para ejecutar una acción EIO y devolver el valor Either String a con el resultado global. Puede leer en monad transformers si desea más detalles.

+0

Muy buena respuesta! –

+0

¡Me encanta el error de mónada! También es ideal para silenciar los mensajes de error falsos en los compiladores ... +1 –

+1

@Norman Ramsey: ¡Bastante bien! En mis primeras aventuras con Haskell, al conocer las bibliotecas que usaban 'Oither' para los errores, rápidamente me molestaba la torpeza de extraer de 'Right' para transmitir los valores; así que escribí funciones que (en retrospectiva) eran casi equivalentes a 'fmap' y' (>> =) '. Casi al mismo tiempo que obtuve el conocimiento para reconocer la estructura monádica, descubrí que 'MonadError' existió todo el tiempo, y me sentí algo tonto por mis crudas reinvenciones.En todo el alboroto sobre las mónadas "sofisticadas", parece que la elegancia de 'Oither' y 'Maybe' monádica se olvida ... –

5

general coincidencia de patrones es una forma mucho mejor que ir a un montón de si las declaraciones, y las condiciones de comprobación de errores no es una excepción:

func :: [Int] -> Either String Int 
func [] = Left "Empty lists are bad" 
func [x] 
    | x < 0 = Left "Negative? Really?" 
    | odd x = Left "Try an even number" 
func xs = Right (length xs) 

Esta función devuelve ya sea un mensaje de error o la longitud del parámetro. Los casos de error se prueban primero y solo si ninguno de ellos coincide con el último caso se ejecuta.

+0

Gracias Es una buena idea. Lo que no mencioné originalmente es que algunas de las condiciones de mi prueba necesitan IO. ¿Hay alguna manera de verificar las condiciones de IO en un protector de patrones? – me2

-1

Use guardias:

f z 
    | failure_1 = ... 
    | failure_2 = ... 
    | failure_3 = ... 
    | failure_4 = ... 
    | otherwise = do_computation 
1

no creo que se puede utilizar IO en un guardia.

En su lugar, se podría hacer algo como esto:

myIoAction filename = foldr ($) [noFile, fileTooLarge, notOnlyFile] do_computation 
    where do_computation 
      = do {- do something -} 
       return (Right answer) 
     noFile success 
      = do {- find out whether file exists -} 
       if {- file exists -} then success else return (Left "no file!") 
     fileTooLarge success 
      = do {- find out size of file -} 
       if maxFileSize < fileSize then return (Left "file too large") else success 
     -- etc 
1

Tomando su other question como una modificación pretendida en este caso, se podría crear algo así como una sentencia switch/case

select :: Monad m => [(m Bool, m a)] -> m a -> m a 
select fallback [] = fallback 
select fallback ((check, action) : others) = do 
    ok <- check 
    if ok then action else select fallback others 

newfile :: FilePath -> IO Bool 
newfile x = select 
    (return True) 
    [ (return $ length x <= 0, return False) 
    , (doesFileExist x,  return False) ] 

aunque esto uno en particular podría escribirse fácilmente

newFile [] = return False 
newFile fn = fmap not $ doesFileExist fn 
Cuestiones relacionadas