What an idiomatic way to handle a lazy input channel in Haskell

I use the IRC bot, and since I connect via SSL using OpenSSL.Session, I use the lazyRead function to read data from the socket. During the initial phase of the connection, I need to do several things: sort the nickname, nickserv authentication, connecting channels, etc.), so there is a certain state. 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) 

Therefore, when I switch to the "Connected" state, I still end up reviewing the case, even if it is really necessary to initialize the connection. Another problem is that adding a nested state will be very painful.

Another way would be to replace mapM with something normal, to process the lines only until we connect, and then run another loop for the rest. This will require either tracking the remaining list, or re-invoking SSL.lazyRead (which is not so bad).

Another solution is to keep the remaining list of lines in state and draw lines when necessary, like getLine .

What is better to do in this case? Is Haskell really lazy to do this so that we go directly to the Connected event after the state stops updating or is the case always strict?

+6
source share
1 answer

You can use the Pipe type from pipes . The trick is that instead of creating a state machine and a transition function, you can code the state implicitly in the Pipe control flow.

Here's what Pipe looks like:

 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 part with the state of your processMessage function. It handles initialization and authentication, but defers further message processing to the next steps by returning msg .

You can then iterate over all the Pipe yield messages with 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) 

Now all you need is a source of ByteString strings to feed to processMessage . You can use the following 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:

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

Note that the manufacturer of lines does not use lazy IO . It will work even if you use the strict ByteString module, but the behavior of the entire program will still be lazy.

If you want to know more about how pipes works, you can read the pipes tutorial .

+5
source

Source: https://habr.com/ru/post/983999/


All Articles