简体   繁体   English

什么是在Haskell中处理延迟输入通道的惯用方法

[英]What's an idiomatic way of handling a lazy input channel in Haskell

I am implementing an IRC bot and since I am connecting over SSL by using OpenSSL.Session I use lazyRead function to read data from the socket. 我正在实现一个IRC机器人,因为我通过使用OpenSSL.Session连接SSL,我使用lazyRead函数从套接字读取数据。 During the initial phase of the connection I need to perform several things in order: nick negotiation, nickserv identification, joining channels etc) so there is some state involved. 在连接的初始阶段,我需要按顺序执行几项操作:缺口协商,缺口服务器识别,加入渠道等)因此涉及一些状态。 Right now I came up with the following: 现在我想出了以下内容:

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)

So when I get to the "Connected" state I still end up going through the case statement even though it's only really needed to initialize the connection. 因此,当我进入“已连接”状态时,我仍然会通过case语句,即使它只是真正需要初始化连接。 The other problem is that adding nested StateT's would be very painful. 另一个问题是添加嵌套的StateT会非常痛苦。

Other way would be to replace mapM with something custom to only process lines until we are connected and then start another loop over the rest. 另一种方法是用自定义的东西替换mapM ,直到我们连接然后再开始另一个循环。 This would require either keeping track of what's left in the list or invoking SSL.lazyRead once again (which is not too bad). 这需要跟踪列表中剩下的内容或再次调用SSL.lazyRead (这不是太糟糕)。

Another solution is to keep the remaining lines list in the state and draw lines when needed similar to getLine . 另一种解决方案是将剩余的行列表保持在状态,并在需要时绘制与getLine类似的行。

What's the better thing to do in this case? 在这种情况下,做什么更好? Would Haskell's laziness make it so that we go directly to Connected case after state stops updating or is case always strict? Haskell的懒惰是否会使我们在状态停止更新后直接进入Connected案例或者case总是严格的?

You can use the Pipe type from pipes . 您可以使用Pipe从类型pipes The trick is that instead of creating a state machine and a transition function you can encode the the state implicitly in the control flow of the Pipe . 诀窍是,您可以隐式地在Pipe的控制流中对状态进行编码,而不是创建状态机和转换函数。

Here is what the Pipe would look like: 这是Pipe样子:

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

The stateful pipe corresponds to the stateful part of your processMessage function. stateful管道对应于processMessage函数的有状态部分。 It handles initialization and authentication, but defers further message processing to downstream stages by re- yield ing the msg . 它处理的初始化和验证,但通过重新推迟进一步的消息处理到下游阶段yield荷兰国际集团的msg

You can then loop over every message this Pipe yield s by using for : 然后,您可以使用for循环遍历此Pipe yield的每条消息:

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)

Now all you need is a source of ByteString lines to feed to processMessage . 现在您只需要一个ByteString行的源来提供给processMessage You can use the following Producer : 您可以使用以下Producer

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

Then you can connect lines to processMessage and run them: 然后,您可以将lines连接到processMessage并运行它们:

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

Note that the lines Producer does not use lazy IO . 请注意,生产者lines不使用惰性IO It will work even if you use the strict ByteString module, but the behavior of the entire program will still be lazy. 即使你使用严格的ByteString模块它也会工作,但整个程序的行为仍然是懒惰的。

If you want to learn more about how pipes works, you can read the pipes tutorial . 如果您想了解有关pipes如何工作的更多信息,可以阅读pipes教程

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM