2011-08-29 9 views
11

Yo estaba jugando con fallos componibles y logró escribir una función con la firmaCómo escribir sin la notación do

getPerson :: IO (Maybe Person) 

donde una persona es:

data Person = Person String Int deriving Show 

funciona y he escrito en el do-notación de la siguiente manera:

import Control.Applicative 

getPerson = do 
    name <- getLine -- step 1 
    age <- getInt -- step 2 
    return $ Just Person <*> Just name <*> age 

donde

getInt :: IO (Maybe Int) 
getInt = do 
    n <- fmap reads getLine :: IO [(Int,String)] 
    case n of 
     ((x,""):[]) -> return (Just x) 
     _ -> return Nothing 

Escribí esta función con la intención de crear posibles fallas compostables. Aunque tengo poca experiencia con mónadas distintas de Maybe y IO, parece que si tuviera un tipo de datos más complicado con muchos más campos, los cálculos de encadenamiento no serían complicados.

Mi pregunta es ¿cómo voy a reescribir esto sin hacer notación? Como no puedo vincular valores a nombres como el nombre o la edad, no estoy seguro de por dónde empezar.

La razón para preguntar es simplemente para mejorar mi comprensión de (>> =) y (< *>) y para redactar fallas y éxitos (no para aclarar mi código con líneas divisorias ilegibles).

Editar: Creo que debería aclarar, "¿cómo debo volver a escribir getPerson sin notación?", No me importa la función getInt la mitad.

+2

Véase también: [Haskell 2010 Informe> Expresiones # Hacer Expresiones] (http://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-470003.14) –

+1

Solo para destacar, tenga en cuenta que 'getPerson' isn No es una función, ya que no tiene '->' en su firma de tipo; si quieres un nombre más preciso que "valor", iría con "IO action". Ver ["¿Todo es una función?" En Haskell?] (Http://conal.net/blog/posts/everything-is-a-function-in-haskell) para más información sobre esto. –

Respuesta

20

Do-notación desugars a (>> =) sintaxis de esta manera:

getPerson = do 
    name <- getLine -- step 1 
    age <- getInt -- step 2 
    return $ Just Person <*> Just name <*> age 

getPerson2 = 
    getLine >>= 
    (\name -> getInt >>= 
    (\age -> return $ Just Person <*> Just name <*> age)) 

cada línea en do-notación, después de la primera, se traduce en un lambda que luego se une a la línea anterior . Es un proceso completamente mecánico para enlazar valores a nombres. No veo cómo el uso de notación do o no afectaría la compibilidad en absoluto; es estrictamente una cuestión de sintaxis.

Su otra función es similar:

getInt :: IO (Maybe Int) 
getInt = do 
    n <- fmap reads getLine :: IO [(Int,String)] 
    case n of 
     ((x,""):[]) -> return (Just x) 
     _ -> return Nothing 

getInt2 :: IO (Maybe Int) 
getInt2 = 
    (fmap reads getLine :: IO [(Int,String)]) >>= 
    \n -> case n of 
     ((x,""):[]) -> return (Just x) 
     _    -> return Nothing 

algunas pistas de la dirección que parecen estar encabezado:

Al utilizar Control.Applicative, a menudo es útil usar <$> para levantar funciones puras en la mónada . Hay una buena oportunidad para ello en la última línea:

Just Person <*> Just name <*> age 

convierte

Person <$> Just name <*> age 

También, usted debe buscar en transformadores mónada. El paquete mtl está más extendido porque viene con la plataforma Haskell, pero hay otras opciones. Los transformadores de mónada le permiten crear una nueva mónada con el comportamiento combinado de las mónadas subyacentes. En este caso, está usando funciones con el tipo IO (Maybe a). El MTL (en realidad una biblioteca de la base, transformadores) define

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) } 

Este es el mismo que el tipo que está usando, con la variable instanciada en mIO.Esto significa que puede escribir:

getPerson3 :: MaybeT IO Person 
getPerson3 = Person <$> lift getLine <*> getInt3 

getInt3 :: MaybeT IO Int 
getInt3 = MaybeT $ do 
    n <- fmap reads getLine :: IO [(Int,String)] 
    case n of 
     ((x,""):[]) -> return (Just x) 
     _    -> return Nothing 

getInt3 es exactamente el mismo, excepto para el MaybeT constructor. Básicamente, cada vez que tenga un m (Maybe a) puede envolverlo en MaybeT para crear un MaybeT m a. Esto gana una capacidad de compilación más simple, como puede ver en la nueva definición de getPerson3. Esa función no se preocupa por el fracaso en absoluto, ya que todo es manejado por la tubería MaybeT. La pieza restante es getLine, que es solo IO String. Esto se levanta en la mónada MaybeT con la función lift.

Editar El comentario de newacct sugiere que también debería proporcionar un ejemplo de coincidencia de patrones; es básicamente lo mismo con una excepción importante. Considere este ejemplo (la lista mónada es la mónada que nos interesa, Maybe es sólo allí por coincidencia de patrones):

f :: Num b => [Maybe b] -> [b] 
f x = do 
    Just n <- x 
    [n+1] 

-- first attempt at desugaring f 
g :: Num b => [Maybe b] -> [b] 
g x = x >>= \(Just n) -> [n+1] 

Aquí g hace exactamente lo mismo que f, pero lo que si el ajuste de patrones falla?

Prelude> f [Nothing] 
[] 

Prelude> g [Nothing] 
*** Exception: <interactive>:1:17-34: Non-exhaustive patterns in lambda 

¿Qué está pasando? Este caso particular es el motivo de una de las verrugas más grandes (OMI) en Haskell, el método Monad de la clase fail. En la notación do, cuando se produce un error en la coincidencia de un patrón, se llama al fail. Una traducción real estaría más cerca de:

g' :: Num b => [Maybe b] -> [b] 
g' x = x >>= \x' -> case x' of 
         Just n -> [n+1] 
         _  -> fail "pattern match exception" 

ahora tenemos

Prelude> g' [Nothing] 
[] 

fail s utilidad depende de la mónada. Para las listas, es increíblemente útil, básicamente haciendo que la coincidencia de patrones funcione en las listas de comprensión. También es muy bueno en la mónada Maybe, ya que un error de coincidencia de patrón daría lugar a un cálculo fallido, que es exactamente cuando Maybe debe ser Nothing. Para IO, quizás no tanto, ya que simplemente arroja una excepción de error de usuario a través de error.

Esa es la historia completa.

+0

Entendí que la notación de do era azúcar de sintaxis para (>> =) pero nunca había visto exactamente cómo. Eso tiene sentido y se parece al cálculo lambda ordinario. En cuanto al resto de tu respuesta, gracias de nuevo. 'Persona <$> Solo nombre <*> edad' es claramente más limpio y es probable que 'levante' funciones puras de ese estilo en el futuro, sea cual sea el ascensor. Lo que me lleva a la tercera parte de su respuesta donde introduce transformadores mtl y mónada. Nunca he leído sobre estos y no sé nada sobre ellos, ya que siempre supuse que estaban más lejos de lo que estaba experimentando. Parece que comenzaré con ellos el próximo. – Dave

+0

Ch. 18 de Real World Haskell (http://book.realworldhaskell.org/read/monad-transformers.html) presenta transformadores de mónada. LYAH lamentablemente no parece cubrirlos. La elevación, en general, significa que tomas algo de un tipo regular y lo colocas en un contexto más elegante. Así que usar '<$>' levanta una función pura en un aplicativo, y la función 'levantar' de un transformador de mónada toma un cálculo en la mónada subyacente y lo eleva a la elegante mónada combinada. –

+1

cuando la cosa en el lado izquierdo de '<-' es un patrón que no está completo, entonces se vuelve más complicado – newacct

4

do -blocks de la desugar forma var <- e1; e2 a expresiones usando >>= como sigue e1 >>= \var -> e2. Así se convierte en el código de getPerson:

getPerson = 
    getLine >>= \name -> 
    getInt >>= \age -> 
    return $ Just Person <*> Just name <*> age 

Como se ve no es muy diferente del código utilizando do.

+0

Gracias, era consciente de que la notación de do fue azucarada (>> =) pero no estoy seguro de cómo exactamente. Me estaba alejando de ghci tratando de obtener el resultado esperado ('mi' esperado) sin éxito. – Dave

+1

Esto parece mucho más agradable que cuando las personas usan parens anidados. – Davorak

1

En realidad, de acuerdo con this explaination, la traducción exacta de su código es

getPerson = 
    let f1 name = 
        let f2 age = return $ Just Person <*> Just name <*> age 
         f2 _ = fail "Invalid age" 
        in getInt >>= f2 
     f1 _ = fail "Invalid name" 
    in getLine >>= f1 

getInt = 
    let f1 n = case n of 
       ((x,""):[]) -> return (Just x) 
       _ -> return Nothing 
     f1 _ = fail "Invalid n" 
    in (fmap reads getLine :: IO [(Int,String)]) >>= f1 

Y la coincidencia de patrón ejemplo

f x = do 
    Just n <- x 
    [n+1] 

traducida a

f x = 
    let f1 Just n = [n+1] 
     f1 _ = fail "Not Just n" 
    in x >>= f1 

Obviamente, este resultado traducida es menos legible que la versión lambda, pero funciona con o sin p coincidencia attern.

Cuestiones relacionadas