2015-03-19 18 views
6

Tôi đang triển khai bot IRC và vì tôi đang kết nối qua SSL bằng cách sử dụng OpenSSL.Session, tôi sử dụng hàm lazyRead để đọc dữ liệu từ ổ cắm. Trong giai đoạn đầu của kết nối, tôi cần phải thực hiện một số điều theo thứ tự: đàm phán nick, nhận dạng nickserv, tham gia các kênh v.v.) để có một số trạng thái liên quan. Ngay bây giờ, tôi đã đưa ra những điều sau đây:Cách thức lý tưởng để xử lý kênh đầu vào lười trong Haskell

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) 

Vì vậy, khi tôi đến trạng thái "Đã kết nối", tôi vẫn sẽ kết thúc câu lệnh khi khởi tạo kết nối. Vấn đề khác là thêm StateT lồng nhau sẽ rất đau đớn.

Cách khác là thay thế mapM bằng thứ gì đó tùy chỉnh để chỉ xử lý các đường cho đến khi chúng tôi kết nối và sau đó bắt đầu một vòng lặp khác trong phần còn lại. Điều này sẽ yêu cầu theo dõi những gì còn lại trong danh sách hoặc gọi lại SSL.lazyRead (không quá tệ).

Một giải pháp khác là giữ cho các dòng còn lại liệt kê trong tiểu bang và vẽ các đường khi cần thiết tương tự như getLine.

Điều tốt hơn nên làm trong trường hợp này là gì? Liệu sự lười biếng của Haskell có làm cho nó để chúng ta đi trực tiếp đến trường hợp Connected sau khi trạng thái dừng cập nhật hay là case luôn nghiêm ngặt?

+0

Một giải pháp thay thế khác là sử dụng [ống dẫn] (https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/conduit-overview). –

Trả lời

5

Bạn có thể sử dụng loại Pipe từ pipes. Bí quyết là thay vì tạo một máy trạng thái và một hàm chuyển tiếp, bạn có thể mã hóa trạng thái ngầm trong luồng điều khiển của Pipe.

Dưới đây là những gì các Pipe sẽ trông như thế:

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 

Các ống stateful tương ứng với phần stateful các chức năng processMessage của bạn. Nó xử lý việc khởi tạo và xác thực, nhưng ngăn chặn việc xử lý tin nhắn tiếp tục đến các giai đoạn hạ lưu bằng cách tái nhập yield vào msg.

Bạn có thể sau đó lặp qua tất cả các thông điệp này Pipeyield s bằng cách sử dụng 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) 

Bây giờ tất cả bạn cần là một nguồn ByteString dòng để nuôi để processMessage. Bạn có thể sử dụng sau đây Producer:

lines :: Producer ByteString IO() 
lines = do 
    bs <- liftIO (ByteString.getLine) 
    if ByteString.null bs 
     then return() 
     else do 
      yield bs 
      lines 

Sau đó, bạn có thể kết nối lines để processMessage và chạy chúng:

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

Lưu ý rằng lines Nhà sản xuất không sử dụng lười biếng IO. Nó sẽ hoạt động ngay cả khi bạn sử dụng mô-đun ByteString nghiêm ngặt, nhưng hành vi của toàn bộ chương trình vẫn sẽ bị chậm.

Nếu bạn muốn tìm hiểu thêm về cách hoạt động của pipes, bạn có thể đọc the pipes tutorial.

+0

Cảm ơn rất nhiều, cố gắng sử dụng Ống ngay bây giờ, chúng trông tuyệt vời! Có tương đương với việc sử dụng 'await' trong' processMessage' sau đó thực hiện 'dòng> -> stateful> -> processMessage'? Ngoài ra, cách tốt nhất để xử lý các trường hợp ngoại lệ xảy ra ở nhà sản xuất để các đường ống khác có cơ hội làm sạch một cách duyên dáng là gì? Ví dụ tôi có thể muốn sinh ra một số luồng trong 'stateful' nhưng tôi muốn giết chúng bất cứ khi nào người tiêu dùng truy cập EOF. – egdmitry

+0

Tôi đoán tôi nên nhìn vào Pipes.Safe. – egdmitry

+0

Ngoài ra, tôi muốn xử lý các thông điệp ping trước tiên, vì vậy tôi chuyển các trạng thái stateful và processMessage nhưng sau đó tôi phải làm một cái gì đó như 'forever await' trong trạng thái của tôi bất cứ khi nào nó hoàn thành việc đăng ký. Điều này có tệ không? – egdmitry

Các vấn đề liên quan