2015-03-19 11 views
6

Wdrażam bota IRC i ponieważ łączę się przez SSL za pomocą OpenSSL.Session używam funkcji lazyRead do odczytu danych z gniazda. W początkowej fazie połączenia muszę wykonać kilka rzeczy w kolejności: negocjowanie nicków, identyfikacja nickserv, łączenie kanałów itd., Więc jest w to zaangażowany jakiś stan. Teraz wpadłem na następujący:Co to jest idiomatyczny sposób obsługi leniwego kanału wejściowego w 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) 

Więc kiedy się do stanu „Connected” I nadal skończyć się poprzez zestawienie przypadku nawet jeśli jest to tylko naprawdę potrzebne, aby zainicjować połączenie. Innym problemem jest to, że dodanie zagnieżdżonych StateT byłoby bardzo bolesne.

Innym sposobem byłoby zastąpienie mapM czymś niestandardowym, aby przetwarzać tylko linie, dopóki nie zostaniemy połączeni, a następnie uruchomimy kolejną pętlę nad resztą. Wymagałoby to albo śledzenia tego, co pozostało na liście, albo ponownego wywołania SSL.lazyRead (co nie jest złe).

Innym rozwiązaniem jest pozostawienie listy pozostałych wierszy w stanie i narysowanie linii w razie potrzeby podobnej do getLine.

Co jest lepsze w tym przypadku? Czy lenistwo Haskella sprawi, że przejdziemy bezpośrednio do sprawy Connected po tym, jak stan przestanie się odnawiać lub będzie zawsze surowy?

+0

Inną alternatywą byłoby użycie [conduit] (https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/conduit-overview). –

Odpowiedz

5

Możesz użyć typu Pipe z pipes. Sztuczka polega na tym, że zamiast tworzyć automat stanów i funkcję przejścia, można zakodować stan domyślnie w strumieniu sterowania Pipe.

Oto co Pipe wyglądałby następująco:

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 

Rura stateful odpowiada stanowej ramach swojej funkcji processMessage. Zajmuje się inicjalizacją i uwierzytelnianiem, ale odradza dalsze przetwarzanie komunikatów do dalszych etapów przez ponowne wygenerowanie msg.

Następnie można pętli nad każdą wiadomość ta Pipeyield ów przy użyciu 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) 

Teraz wszystko, czego potrzebujesz jest źródłem ByteString linii karmić do processMessage. Można użyć następujących Producer:

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

Następnie można połączyć lines do processMessage i uruchomić je:

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

Zauważ, że lines Producent nie używa leniwe IO. To zadziała, nawet jeśli użyjesz surowego modułu ByteString, ale zachowanie całego programu nadal będzie leniwy.

Jeśli chcesz dowiedzieć się więcej o tym, jak działa pipes, przeczytaj artykuł the pipes tutorial.

+0

Wielkie dzięki, próbując teraz Pipesa, wyglądają świetnie! Czy jest to równoważne użyciu 'await' w' processMessage', a następnie wykonanie 'lines> -> stateful> -> processMessage'? Ponadto, jaki jest najlepszy sposób postępowania z wyjątkami w producencie, aby inne rury miały szansę oczyścić się z gracji? Na przykład mogę chcieć odrodzić niektóre wątki w "stateful", ale chciałbym je zabić za każdym razem, gdy konsument trafi EOF. – egdmitry

+0

Chyba powinienem zajrzeć do Pipes.Safe. – egdmitry

+0

Chciałbym również najpierw obsługiwać wiadomości ping, więc przełączam się na miejsca stanowe i processMessage, ale muszę zrobić coś w stylu "na zawsze oczekuję" w moim stanowym, gdy w pełni zakończy rejestrację. Czy to w jakimś sensie takie złe? – egdmitry

Powiązane problemy