首页 文章

用状态从wiki.haskell.org扩展IRC bot

提问于
浏览
1

问题

我正试图从https://wiki.haskell.org/Roll_your_own_IRC_bot扩展IRC机器人,其中有一些状态是's updated every time the bot posts a message in the channel it'连接到 .

该功能是:每次在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和State,

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

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

  • 在Haskell IRC Channels 中提问 .

问题

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

1 回答

  • 2

    我只需在你的main中添加一个 >> 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

相关问题