2015-03-19 8 views
6

Sto implementando un bot IRC e dal momento che mi sto collegando su SSL utilizzando OpenSSL.Session, utilizzo la funzione lazyRead per leggere i dati dal socket. Durante la fase iniziale della connessione ho bisogno di eseguire diverse cose in ordine: negoziazione del nick, identificazione del nickserver, collegamento ai canali ecc. Quindi c'è qualche stato coinvolto. In questo momento mi si avvicinò con il seguente:Che cos'è un modo idiomatico di gestire un canale di input pigro in Haskell

data ConnectionState = Initial | NickIdentification | Connected 

listen :: SSL.SSL -> IO() 
listen ssl = do 
    lines <- BL.lines `fmap` SSL.lazyRead ssl 
    evalStateT (mapM_ (processLine ssl) lines) Initial 

processLine :: SSL.SSL -> BL.ByteString -> StateT ConnectionState IO() 
processLine ssl line = do case message of 
          Just a -> processMessage ssl a 
          Nothing -> return() 
    where message = IRC.decode $ BL.toStrict line 

processMessage :: SSL.SSL -> IRC.Message -> StateT ConnectionState IO() 
processMessage ssl m = do 
    state <- S.get 
    case state of 
     Initial -> when (IRC.msg_command m == "376") $ do 
     liftIO $ putStrLn "connected!" 
     liftIO $ privmsg ssl "NickServ" ("identify " ++ nick_password) 
     S.put NickIdentification 
     NickIdentification -> do 
     when (identified m) $ do 
      liftIO $ putStrLn "identified!" 
      liftIO $ joinChannel ssl chan 
      S.put Connected 
     Connected -> return() 
    liftIO $ print m 
    when (IRC.msg_command m == "PING") $ (liftIO . pong . mconcat . map show) (IRC.msg_params m) 

Così quando arrivo allo stato "Connected" Ho ancora finiscono per passare attraverso l'istruzione case anche se è veramente solo bisogno di inizializzare la connessione. L'altro problema è che aggiungere StateT annidati sarebbe molto doloroso.

Un altro modo sarebbe quello di sostituire mapM con qualcosa di personalizzato per elaborare solo le linee fino a quando non siamo connessi e quindi avviare un altro ciclo sul resto. Ciò richiederebbe di tenere traccia di ciò che è rimasto nella lista o invocando ancora una volta SSL.lazyRead (che non è male).

Un'altra soluzione consiste nel mantenere l'elenco di righe rimanenti nello stato e disegnare le righe quando necessario in modo simile a getLine.

Qual è la cosa migliore da fare in questo caso? La pigrizia di Haskell è tale da andare direttamente al caso Connected dopo l'interruzione dello stato o case è sempre rigoroso?

+0

Un'altra alternativa sarebbe utilizzare [conduit] (https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/conduit-overview). –

risposta

5

È possibile utilizzare il tipo Pipe da pipes. Il trucco è che invece di creare una macchina a stati e una funzione di transizione è possibile codificare lo stato implicitamente nel flusso di controllo dello Pipe.

Ecco ciò che il Pipe sarà simile:

stateful :: Pipe ByteString ByteString IO r 
stateful = do 
    msg <- await 
    if (IRC.msg_command msg == "376") 
     then do 
      liftIO $ putStrLn "connected!" 
      liftIO $ privmsg ssl "NickServ" ("identify " ++ nick_password) 
      yield msg 
      nick 
     else stateful 

nick :: Pipe ByteString ByteString IO r 
nick = do 
    msg <- await 
    if identified msg 
     then do 
      liftIO $ putStrLn "identified!" 
      liftIO $ joinChannel ssl chan 
      yield msg 
      cat -- Forward the remaining input to output indefinitely 
     else nick 

Il stateful tubazione corrisponde alla parte stateful della funzione processMessage. Gestisce l'inizializzazione e l'autenticazione, ma rinvia l'ulteriore elaborazione dei messaggi agli stadi downstream mediante re yield in msg.

È quindi possibile eseguire un ciclo su ogni messaggio questo Pipeyield s utilizzando for:

processMessage :: Consumer ByteString IO r 
processMessage = for stateful $ \msg -> do 
    liftIO $ print m 
    when (IRC.msg_command m == "PING") $ (liftIO . pong . mconcat . map show) (IRC.msg_params m) 

Ora tutto ciò che serve è una fonte di ByteString linee di mangimi per processMessage. È possibile utilizzare il seguente Producer:

lines :: Producer ByteString IO() 
lines = do 
    bs <- liftIO (ByteString.getLine) 
    if ByteString.null bs 
     then return() 
     else do 
      yield bs 
      lines 

quindi è possibile collegare lines a processMessage ed eseguirli:

runEffect (lines >-> processMessage) :: IO() 

Nota che il produttore lines non usa pigro IO. Funzionerà anche se si utilizza il modulo ByteString rigoroso, ma il comportamento dell'intero programma sarà ancora pigro.

Se si desidera ulteriori informazioni su come funziona pipes, è possibile leggere the pipes tutorial.

+0

Grazie mille, provando Pipes ora, sono fantastici! È equivalente all'usare 'await' in' processMessage', quindi a 'lines> -> stateful> -> processMessage'? Inoltre, qual è il modo migliore per gestire le eccezioni verificatesi nel produttore in modo che altre pipe possano ripulire in modo elegante? Per esempio potrei voler generare alcuni thread in 'stateful', ma vorrei ucciderli ogni volta che il consumatore colpisce EOF. – egdmitry

+0

Immagino che dovrei dare un'occhiata a Pipes.Safe. – egdmitry

+0

Inoltre, mi piacerebbe prima gestire i messaggi di ping, quindi cambio posizione stateful e processMessage ma poi devo fare qualcosa di simile a "attendere sempre" nel mio stato ogni volta che completa completamente la registrazione. È cattivo in qualsiasi senso? – egdmitry

Problemi correlati