2011-08-29 16 views
11

stavo giocando intorno con fallimenti componibili ed è riuscito a scrivere una funzione con la firmaCome scrivere senza Do notazione

getPerson :: IO (Maybe Person) 

in cui una persona è:

data Person = Person String Int deriving Show 

funziona e ho scritto nel fai-notazione come segue:

import Control.Applicative 

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

dove

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

Ho scritto questa funzione con l'intento di creare possibili errori componibili. Anche se ho poca esperienza con le monade diverse da Maybe e IO, sembra che se avessi un tipo di dati più complicato con molti altri campi, il calcolo della concatenazione non sarebbe complicato.

La mia domanda è come potrei riscrivere questo senza la notazione? Dal momento che non riesco a legare valori a nomi come nome o età, non sono davvero sicuro da dove cominciare.

La ragione per chiedere è semplicemente di migliorare la mia comprensione di (>> =) e (< *>) e di comporre errori e successi (non per indovinare il mio codice con one-liner illeggibili).

Edit: Penso che dovrei chiarire, "come dovrei riscrivere il getPerson senza la notazione", non mi interessa la metà della funzione getInt.

+2

Vedere anche: [Rapporto Haskell 2010> Espressioni # Espressioni Do] (http://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-470003.14) –

+1

Solo per fare i pasticci, si noti che 'getPerson' isn 'una funzione, poiché non ha '->' nella sua firma del tipo; se vuoi un nome più preciso di "valore", andrei con "IO action". Vedi ["Tutto è una funzione" in Haskell?] (Http://conal.net/blog/posts/everything-is-a-function-in-haskell) per ulteriori informazioni. –

risposta

20

Do-notazione desugars a (>> =) sintassi in questo modo:

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)) 

ogni linea di fai notazione, dopo il primo, si traduce in un lambda che viene quindi associato alla riga precedente . È un processo completamente meccanico per legare valori ai nomi. Non vedo come l'uso della notazione o non influenzi affatto la componibilità; è strettamente una questione di sintassi.

tua altra funzione è simile:

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 

Alcune indicazioni per la direzione ti sembra di essere a capo:

Quando si utilizza Control.Applicative, è spesso utile utilizzare <$> per sollevare funzioni pure nella monade . C'è una buona opportunità per questa nell'ultima riga:

Just Person <*> Just name <*> age 

diventa

Person <$> Just name <*> age 

Inoltre, si dovrebbe guardare in trasformatori monade. Il pacchetto mtl è molto diffuso perché include la piattaforma Haskell, ma ci sono altre opzioni. I trasformatori di Monad ti permettono di creare una nuova monade con il comportamento combinato delle monadi sottostanti. In questo caso, stai utilizzando le funzioni con il tipo IO (Maybe a). Il mtl (in realtà una libreria base, trasformatori) definisce

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

Questo è lo stesso del tipo in uso, con la variabile m un'istanza a IO.Ciò significa che è possibile scrivere:

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 è esattamente lo stesso, tranne per il costruttore MaybeT. In pratica, ogni volta che si dispone di un m (Maybe a) è possibile eseguire il wrapping in MaybeT per creare un MaybeT m a. Ciò consente di ottenere una composizione più semplice, come si può vedere dalla nuova definizione di getPerson3. Quella funzione non si preoccupa affatto del fallimento perché è tutto gestito dall'impianto idraulico di MaybeT. L'altro pezzo è getLine, che è solo un IO String. Questo viene sollevato nella monade MaybeT dalla funzione lift.

Modifica Il commento di newacct suggerisce che dovrei fornire anche un esempio di corrispondenza del modello; è praticamente lo stesso con un'eccezione importante. Si consideri questo esempio (l'elenco monade è la monade che ci interessa, Maybe è lì solo per pattern matching):

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] 

Qui g fa esattamente la stessa cosa di f, ma cosa succede se il pattern match fallisce?

Prelude> f [Nothing] 
[] 

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

Cosa sta succedendo? Questo caso particolare è la ragione per una delle più grandi verruche (IMO) in Haskell, il metodo fail della classe. Nella notazione, quando un pattern match fallisce, viene chiamato fail. Una traduzione reale sarebbe quello più vicino a:

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

ora abbiamo

Prelude> g' [Nothing] 
[] 

fail s utilità dipende dalla monade. Per gli elenchi, è incredibilmente utile, fondamentalmente facendo in modo che il pattern matching funzioni nelle list comprehensions. È anche molto buono nella monade Maybe, dal momento che un errore di corrispondenza del modello porterebbe a un calcolo fallito, che è esattamente quando Maybe dovrebbe essere Nothing. Per IO, forse non così tanto, in quanto genera semplicemente un'eccezione di errore utente tramite error.

Questa è la storia completa.

+0

Ho capito che la notazione era lo zucchero di sintassi per (>> =) ma non avevo mai visto esattamente come. Questo ha senso e assomiglia al normale calcolo lambda. Per il resto della tua risposta, grazie ancora. 'Persona <$> Il solo nome <*>' età è chiaramente più ordinato e probabilmente aumenterò 'pure funzioni simili in futuro, qualunque sia l'ascensore. Questo mi porta alla terza parte della risposta in cui si introducono i trasformatori mtl e monad. Non ho mai letto di questi e non ne so nulla, perché ho sempre pensato che fossero più lontani da quello che stavo sperimentando. Sembra che inizierò con loro il prossimo. – Dave

+0

cap. 18 di Real World Haskell (http://book.realworldhaskell.org/read/monad-transformers.html) introduce i trasformatori monad. LYAH sfortunatamente non sembra coprirli. Sollevare, in generale, significa prendere qualcosa di un tipo normale e inserirlo in un contesto più elaborato. Quindi l'uso di '<$>' eleva una funzione pura in un applicativo, e la funzione 'lift' di un trasformatore monad prende un calcolo nella monade sottostante e lo solleva nella monade combinata. –

+1

quando la cosa sul lato sinistro di '<-' è un pattern che non è completo, diventa più complicato – newacct

4

do -blocks della forma var <- e1; e2 desugar a espressioni utilizzando >>= come segue e1 >>= \var -> e2. Così il codice diventa getPerson:

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

Come vedi questo non è molto diverso dal codice utilizzando do.

+0

Grazie, sapevo che la notazione era zuccherata (>> =) ma non sapevo esattamente come. Stavo svolazzando a ghci cercando di ottenere l'esito atteso ("il mio" previsto) senza successo. – Dave

+1

Questo sembra molto più piacevole allora quando le persone usano parents annidati. – Davorak

1

In realtà, secondo this explaination, la traduzione esatta del vostro codice è

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 

E il pattern match esempio

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

tradotto in

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

Ovviamente, questo risultato tradotto è meno leggibile rispetto alla versione lambda, ma funziona con o senza p corrispondenza attern.

Problemi correlati