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

egd*_*try 6 irc monads state haskell

我正在实现一个IRC机器人,因为我通过使用OpenSSL.Session连接SSL,我使用lazyRead函数从套接字读取数据.在连接的初始阶段,我需要按顺序执行几项操作:缺口协商,缺口服务器识别,加入渠道等)因此涉及一些状态.现在我想出了以下内容:

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)
Run Code Online (Sandbox Code Playgroud)

因此,当我进入"已连接"状态时,我仍然会通过case语句,即使它只是真正需要初始化连接.另一个问题是添加嵌套的StateT会非常痛苦.

其他方法是替换mapM自定义的东西只处理线,直到我们连接,然后开始另一个循环.这需要跟踪列表中剩下的内容或SSL.lazyRead再次调用(这不是太糟糕).

另一个解决方案是将剩余的行列表保持在状态,并在需要时绘制线条getLine.

在这种情况下,做什么更好?Haskell的懒惰是否会使我们Connected在状态停止更新后直接进入案例或case总是严格?

Gab*_*lez 5

您可以使用Pipe来自的类型pipes.诀窍是,不是创建状态机和转换函数,而是可以隐式地在状态机的控制流中对状态进行编码Pipe.

这是什么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
Run Code Online (Sandbox Code Playgroud)

stateful管对应于你的状态的部分processMessage功能.它处理的初始化和验证,但通过重新推迟进一步的消息处理到下游级yield荷兰国际集团的msg.

然后,您可以Pipe yield使用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)
Run Code Online (Sandbox Code Playgroud)

现在你所需要的只是一个可以输入的ByteString行的来源processMessage.您可以使用以下内容Producer:

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

然后,您可以连接linesprocessMessage并运行它们:

runEffect (lines >-> processMessage) :: IO ()
Run Code Online (Sandbox Code Playgroud)

请注意,linesProducer不使用lazy IO.即使你使用strict ByteString模块它也会工作,但整个程序的行为仍然是懒惰的.

如果您想了解有关pipes工作原理的更多信息,可以阅读pipes教程.