[英]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.