簡體   English   中英

Haskell懶惰 - 我如何更快地強制IO發生?

[英]Haskell laziness - how do I force the IO to happen sooner?

我剛開始學習Haskell。 下面是一些以強制性風格編寫的代碼,它實現了一個簡單的服務器 - 它打印出HTTP請求頭。 除了我需要在Haskell中重新思考它,使用惰性列表和更高階函數之外,我還想清楚地看到它為什么不能按照我的意圖行事。 它總是一個落后 - 我用一個請求命中它,沒有任何反應,再次點擊它,它打印第一個請求,第三次點擊它,它打印第二個請求,等等。為什么? 什么是對此代碼的最小更改,會導致它在請求進入時正確打印?

import Network
import System.IO
import Network.HTTP.Headers

acceptLoop :: Socket -> IO ()
acceptLoop s = do
  (handle, hostname, _) <- accept s
  putStrLn ("Accepted connection from " ++ hostname)
  text <- hGetContents handle
  let lns = lines text
      hds = tail lns
  print $ parseHeaders hds
  hClose handle
  acceptLoop s


main :: IO ()
main = do
  s <- listenOn (PortNumber 8080)
  acceptLoop s

謝謝,羅布

跟進

所有的答案都很有幫助。 下面的代碼可以工作,但不會像建議的那樣使用字節串。 一個后續問題:可以使用標准庫中的某些函數替換ioTakeWhile ,也許在Control.Monad中?

ioTakeWhile :: (a -> Bool) -> [IO a] -> IO [a]
ioTakeWhile pred actions = do
  x <- head actions
  if pred x
    then (ioTakeWhile pred (tail actions)) >>= \xs -> return (x:xs)
    else return []

acceptLoop :: Socket -> IO ()
acceptLoop s = do
  (handle, hostname, _) <- accept s
  putStrLn ("Accepted connection from " ++ hostname)
  let lineActions = repeat (hGetLine handle)
  lines <- ioTakeWhile (/= "\r") lineActions
  print lines
  hClose handle

您的問題是使用hGetContents將獲取句柄上的所有內容,直到套接字關閉。 您通過嘗試解析輸入的最后一行來跟隨此調用,直到連接終止才會知道。

解決方案:獲取所需數據(或可用數據),然后終止連接。

已經太晚了,我已經累了,但這里有一個解決方案,我知道它不是最優的(讀:丑陋的罪):你可以轉到字節 (無論如何應該這樣做)並使用hGetNonBlockinghGetSome而不是hGetContents 或者,您可以持續hGetLine (阻止),直到解析成功滿足您的要求:

import Network
import System.IO
import Network.HTTP.Headers
import Control.Monad
import qualified Data.ByteString.Char8 as B
import Data.ByteString (hGetSome)

acceptLoop :: Socket -> IO ()
acceptLoop s = do
    (handle, hostname, _) <- accept s
    putStrLn ("Accepted connection from " ++ hostname)
    printHeaders handle B.empty
    hClose handle
  where
  printHeaders h s = do
  t <- hGetSome h 4096
  let str  = B.append s t -- inefficient!
      loop = printHeaders h str
  case (parseHeaders . tail . lines) (B.unpack str) of
      Left _   -> loop
      Right x
       | length x < 3 -> loop
       | otherwise    -> print x

main :: IO ()
main = do
  hSetBuffering stdin NoBuffering
  s <- listenOn (PortNumber 8080)
  forever $ acceptLoop s

該方法的簡要概述:

懶惰程序中的“控制流”與您習慣的不同。 事情不會被評估,直到它們為什么你的程序始終是輸出后面的請求。

一般來說,你可以使用“bang”操作符來制作嚴格的東西! BangPatterns pragma。

如果您在這種情況下使用它(通過說!text <- hGetContents handle ),一旦請求完成,您將獲得標題的輸出。 不幸的是, hGetContents不知道何時在print語句之前停止等待更多數據,因為handle未關閉。

如果您另外重構程序以在let語句和print 之前使用hClose handle ,那么程序的行為就像您想要的那樣。

在另一種情況下,不評估print因為text的值永遠不會通過關閉handle “完成”。 因為它是“懶惰的”,然后print等待hdslns ,它們依次等待text ,等待hClose ...這就是為什么你得到了奇怪的行為; 在下一個請求需要套接字之前,沒有評估hClose ,這就是為什么在此之前沒有輸出的原因。

請注意,簡單地使text嚴格仍然會永遠阻止程序,讓它“等待”文件關閉。 但是,如果文件在text非嚴格時關閉,它將始終為空,並導致錯誤。 同時使用它們將獲得所需的效果。


您的程序包含建議的更改:

進行了三項更改:我添加了{-# LANGUAGE BangPatterns #-} pragma, text前面的單個字符( ! ),並將hClose handle移動了幾行。

{-# LANGUAGE BangPatterns #-}
import Network
import System.IO
import Network.HTTP.Headers

acceptLoop :: Socket -> IO ()
acceptLoop s = do
  (handle, hostname, _) <- accept s
  putStrLn ("Accepted connection from " ++ hostname)
  !text <- hGetContents handle
  hClose handle
  let lns = lines text
      hds = tail lns
  print $ parseHeaders hds
  acceptLoop s

main :: IO ()
main = do
  s <- listenOn (PortNumber 8080)
  acceptLoop s

另一種方法:

要完全回避這樣的問題,您可以嘗試使用System.IO.Strict模塊中的hGetContents函數而不是System.IO


最后一點:

而不是在acceptLoop顯式遞歸,我發現以下main更加慣用:

main = do
  s <- listenOn (PortNumber 8080)
  sequence_ $ repeat $ acceptLoop s

這樣做,您可以從acceptLoop刪除遞歸調用。

TomMD的解決方案forever使用Contol.Monad模塊,這也很好。

您應該對消息何時完成有一些概念。 您需要從片段中的輸入句柄中讀取,直到您意識到您已收到完整的消息。 然后假設之后的所有內容都是下一條消息。 消息可能不會同時出現,也可能成組出現。

例如,消息可能總是固定長度。 或以\\n\\n終止(我相信這是HTTP請求的情況)

[我可能會回來並發布代碼以獲得此建議,但如果我不這樣做,請嘗試調整TomMD的代碼,這是朝着正確方向邁出的一步]

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM