使用state从wiki.haskell.org扩展IRC bot

时间:2015-10-13 04:53:01

标签: haskell bots irc

问题

我正在尝试从https://wiki.haskell.org/Roll_your_own_IRC_bot扩展IRC机器人,并且每次机器人在其连接的通道中发布消息时都会更新。

该功能是:每次在IRC频道中发出命令!last said时,机器人都应该使用时间戳进行响应。为了支持这一点,privmsg函数需要更新机器人的状态 - 特别是lastPosted记录 - 每次调用时都会使用新的时间戳。

到目前为止工作

我从Haskell维基页面底部(使用ReaderT访问有关机器人环境的信息)获取代码,并尝试更改状态转换器(StateT)的ReaderT。结果如下,你可以看到,我没有走得太远。

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

探索其他支持途径

  • 花了几天时间阅读ReaderT,StateT及其非变形金刚朋友Reader and State,

  • 检查具有类似问题的任何人的Stack Overflow,但唯一的其他IRC机器人问题将套接字作为参数连接到需要它的每个函数(而不是使用ReaderT),

  • Tweeted Don S.维基页面的原作者

  • 在Haskell IRC频道中提问。

问题

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

1 个答案:

答案 0 :(得分:2)

我只需在您的主要内容>> return ()的定义中添加loop即可编译您的代码:

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

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

  • runStateT - 返回最终状态和返回值
  • evalStateT - 仅返回最终值
  • execStateT - 仅返回最终状态

loop的原始定义是返回一对(来自runStateT),并且这不会进行类型检查,因为main想要一个只返回()的计算。

要更新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