2015-03-19 11 views
6

Ich bin ein IRC-Bot implementieren und da ich über SSL mithilfe von OpenSSL.Session verbinden, verwende ich lazyRead Funktion, um Daten aus dem Socket zu lesen. Während der Anfangsphase der Verbindung muss ich einige Dinge in der Reihenfolge ausführen: Nick-Verhandlung, NickServ-Identifikation, Verbinden von Kanälen usw.), so dass ein bestimmter Zustand involviert ist. Im Augenblick kam ich mit dem folgenden:Was ist eine idiomatische Art der Handhabung eines Lazy-Eingangskanal 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) 

Also, wenn ich auf den „Connected“ Zustand erhalte ich am Ende noch durch den Fall Aussage gehe, obwohl es eigentlich nur benötigt, um die Verbindung zu initialisieren. Das andere Problem ist, dass das Hinzufügen von verschachtelten StateTs sehr schmerzhaft wäre.

Eine andere Möglichkeit wäre, mapM durch etwas zu ersetzen, das nur Linien verarbeitet, bis wir verbunden sind, und dann eine andere Schleife über den Rest zu starten. Dies würde erfordern, entweder zu verfolgen, was in der Liste übrig ist oder SSL.lazyRead noch einmal aufzurufen (was nicht zu schlecht ist).

Eine andere Lösung besteht darin, die verbleibende Linienliste im Zustand zu halten und bei Bedarf Linien zu zeichnen, ähnlich wie getLine.

Was ist in diesem Fall das Bessere? Würde Haskells Trägheit es so machen, dass wir direkt zu Connected Fall gehen, nachdem der Staat aufhört zu aktualisieren oder ist case immer streng?

+0

Eine andere Alternative wäre [Conduit] (https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/conduit-overview) zu verwenden. –

Antwort

5

Sie können den Typ Pipe von pipes verwenden. Der Trick besteht darin, dass Sie anstelle einer Zustandsmaschine und einer Übergangsfunktion den Status implizit in den Kontrollfluss der Pipe codieren können.

Hier ist, was die Pipe aussehen würde:

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 

Das stateful Rohr entspricht der Stateful-Teil Ihrer processMessage Funktion. Es behandelt die Initialisierung und Authentifizierung, verschiebt jedoch die Weiterverarbeitung der Nachricht zu den nachgeschalteten Stufen durch die .

Sie können dann Schleife über jede Nachricht dieses Pipeyield s von for mit:

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) 

Alles was Sie jetzt eine Quelle der ByteString Linien müssen processMessage zu ernähren. Sie können die folgenden Producer verwenden:

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

Dann Sie lines zu processMessage verbinden und führen Sie sie:

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

Beachten Sie, dass die lines Producer nicht faul IO nicht verwendet. Es funktioniert auch, wenn Sie das strenge ByteString Modul verwenden, aber das Verhalten des gesamten Programms wird immer noch faul sein. Wenn Sie mehr darüber erfahren möchten, wie pipes funktioniert, können Sie the pipes tutorial lesen.

+0

Vielen Dank, versuchen Pipes jetzt, sie sehen super aus! Ist es äquivalent zur Verwendung von 'await' in' processMessage' und dann 'lines> -> stateful> -> processMessage'? Und was ist der beste Weg, um Exceptions zu bearbeiten, die im Producer aufgetreten sind, damit andere Pipes eine Chance haben, sauber zu säubern? Zum Beispiel möchte ich einige Threads in 'stateful' erstellen, aber ich würde sie gerne beenden, wenn der Verbraucher EOF erreicht. – egdmitry

+0

Ich denke, ich sollte in Pipes.Safe schauen. – egdmitry

+0

Ich würde auch gerne zuerst mit Ping-Nachrichten umgehen, also wechsle ich Stateful und ProcessMessage-Orte, aber dann muss ich etwas wie "Für immer warten" in meinem Stateful tun, wenn die Registrierung vollständig abgeschlossen ist. Ist das in irgendeiner Weise schlecht? – egdmitry