使用状态从 wiki.haskell.org 扩展 IRC 机器人

Extending the IRC bot from wiki.haskell.org with state

问题

我正在尝试从 https://wiki.haskell.org/Roll_your_own_IRC_bot 扩展 IRC 机器人,每次机器人 post 在其连接的频道中发送消息时都会更新一些状态。

特点是:每次在IRC频道中发出命令!last said,bot都会回复一个时间戳。为了支持这一点,privmsg 函数需要在每次调用时使用新的时间戳更新机器人的状态——特别是 lastPosted 记录。

到目前为止工作

我从 Haskell wiki 页面底部获取代码(使用 ReaderT 访问有关机器人环境的信息)并尝试更改 Reader T 代表状态转换器 (StateT)。结果如下,如您所见,我并没有走得太远。

import Data.List
import Network
import System.IO
import System.Exit
import System.Time
import Control.Arrow
import Control.Monad.State
import Control.Exception
import Text.Printf

server = "irc.freenode.org"
port   = 6667
chan   = "#testbot-test"
nick   = "testbottest"

-- The 'Net' monad, a wrapper over IO, carrying the bot's immutable state.
type Net = StateT Bot IO
data Bot = Bot { socket :: Handle, lastPosted :: ClockTime }

-- Set up actions to run on start and end, and run the main loop
main :: IO ()
main = bracket connect disconnect loop
  where
    disconnect = hClose . socket
    loop st    = runStateT run st

-- Connect to the server and return the initial bot state
connect :: IO Bot
connect = notify $ do
  h <- connectTo server (PortNumber (fromIntegral port))
  t <- getClockTime
  hSetBuffering h NoBuffering
  return (Bot h t)
    where
      notify a = bracket_
        (printf "Connecting to %s ... " server >> hFlush stdout)
        (putStrLn "done.")
        a

-- We're in the Net monad now, so we've connected successfully
-- Join a channel, and start processing commands
run :: Net ()
run = do
  write "NICK" nick
  write "USER" (nick ++ " 0 * :test bot")
  write "JOIN" chan
  gets socket >>= listen

-- Process each line from the server
listen :: Handle -> Net ()
listen h = forever $ do
  s <- init `fmap` liftIO (hGetLine h)
  liftIO (putStrLn s)
  if ping s then pong s else eval (clean s)
  where
    forever a = a >> forever a
    clean     = drop 1 . dropWhile (/= ':') . drop 1
    ping x    = "PING :" `isPrefixOf` x
    pong x    = write "PONG" (':' : drop 6 x)

-- Dispatch a command
eval :: String -> Net ()
eval     "!quit"               = write "QUIT" ":Exiting" >> liftIO (exitWith ExitSuccess)
-- Posting when something was last posted shouldn't count as last posted.
eval     "!last said"          = getLastPosted >>= (\t -> write "PRIVMSG" (chan ++ " :" ++ t))
eval x | "!id " `isPrefixOf` x = privmsg (drop 4 x)
eval     _                     = return () -- ignore everything else

getLastPosted :: Net String
getLastPosted = do
  t <- gets lastPosted
  return $ show t

-- Send a privmsg to the current chan + server
privmsg :: String -> Net ()
privmsg s = write "PRIVMSG" (chan ++ " :" ++ s)

-- Send a message out to the server we're currently connected to
write :: String -> String -> Net ()
write s t = do
    h <- gets socket
    liftIO $ hPrintf h "%s %s\r\n" s t
    liftIO $ printf    "> %s %s\n" s t

探索其他支持途径

问题

如何将 Haskell wiki IRC bot 扩展为 post 一条消息,其中包含最后一条消息的日期和时间戳 posted?最好使用像 ReaderT 这样的抽象(只允许可变状态)而不是在函数参数中传递状态。

我通过简单地在你的 main 中的 loop 的定义中添加一个 >> return () 来编译你的代码:

main :: IO ()
main = bracket connect disconnect loop
  where
    disconnect = hClose . socket
    loop st    = (runStateT run st) >> return ()

这实际上忽略了 runStateT 的 return 值。以下是 runState/runStateT 的所有变体:

  • runStateT - return 最终状态和 returned 值
  • evalStateT - return 只有最终值
  • execStateT - return只有最终状态

您对 loop 的原始定义是 returning 一对(来自 runStateT),并且这没有类型检查,因为 main 想要一个 return 的计算只是 ().

要更新 lastPosted 字段,请考虑将此添加到 eval 函数,该函数在机器人发送消息 !update time:

时触发
eval "!update time"
     = do t <- liftIO getClockTime
          bot <- get
          put (bot { lastPosted = t })

我们需要 liftIO getClockTime 因为我们在 Net monad 中操作。 然后我们 get 旧状态和 put 更新状态。您可以在 Net monad.

中任何您想更新 lastPosted 时间的地方添加此逻辑

完整代码位于:http://lpaste.net/142931