Haskell懒惰 - 我如何更快地强制IO发生?

Rob*_*b N 14 io haskell lazy-evaluation

我刚开始学习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
Run Code Online (Sandbox Code Playgroud)

谢谢,罗布

跟进

所有的答案都很有帮助.下面的代码可以工作,但不会像建议的那样使用字节串.后续问题:可以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
Run Code Online (Sandbox Code Playgroud)

Tho*_*son 10

您的问题是使用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
Run Code Online (Sandbox Code Playgroud)


Ezr*_*zra 6

该方法的简要概述:

懒惰程序中的"控制流"与您习惯的不同.事情不会被评估,直到它们为什么你的程序始终是输出后面的请求.

一般来说,你可以使用"bang"运算符!BangPatternspragma 来制作严格的东西.

如果您在这种情况下使用它(通过说!text <- hGetContents handle),一旦请求完成,您将获得标题的输出.不幸的是,hGetContents不知道何时在print声明之前停止等待更多数据,因为handle没有关闭.

如果您另外重构程序以使其具有hClose handle 两个let语句print,那么程序的行为就像您想要的那样.

在另一种情况下,print不进行评估,因为text关闭时永远不会"完成" handle.因为它是"懒",print然后等待hdslns,后者又等待text,它在等待hClose......这就是为什么你越来越怪异的行为; hClose直到下一个请求需要套接字才被评估,这就是为什么在此之前没有输出.

请注意,只是make textstrict仍会永远阻止程序,让它"等待"文件关闭.但是,如果文件在text非严格时关闭,它将始终为空,并导致错误.同时使用它们将获得所需的效果.


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

进行了三项更改:我添加了{-# LANGUAGE BangPatterns #-}编译指示,!前面有一个单个字符()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
Run Code Online (Sandbox Code Playgroud)

另一种方法:

要完全回避这样的问题,您可以尝试使用模块中的hGetContents函数System.IO.Strict而不是System.IO.


最后一点:

而不是显式递归acceptLoop,我发现以下main是更惯用的:

main = do
  s <- listenOn (PortNumber 8080)
  sequence_ $ repeat $ acceptLoop s
Run Code Online (Sandbox Code Playgroud)

这样做,您可以从中删除递归调用acceptLoop.

TomMD的解决方案使用forever了该Contol.Monad模块,这也很好.