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

前端 未结 3 1096
暗喜
暗喜 2021-01-02 00:12

I just started learning Haskell. Below is some code written in an imperative style that implements a simple server -- it prints out the HTTP request headers. Besides the f

相关标签:
3条回答
  • 2021-01-02 00:37

    Your problem is using hGetContents will get all contents on the handle till the socket closes. You follow this call by trying to parse the last line of the input, which won't be known till the connection terminates.

    The solution: get as much data as you need (or is available) then terminate the connection.

    It's late and I'm tired but here's a solution I know is non-optimal (read: ugly as sin): You can move to bytestrings (should do this anyway) and use hGetNonBlocking or hGetSome instead of hGetContents. Alternatively, you can hGetLine (blocking) continually till the parse succeeds to your satisfaction:

    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
    
    0 讨论(0)
  • 2021-01-02 00:39

    A brief overview of the approach:

    The "flow of control" in lazy programs is different than you're used to. Things won't be evaluated until they have to which is why your program is always a request behind with the output.

    In general, you can make something strict by using the "bang" operator ! and the BangPatterns pragma.

    If you use it in this case (by saying !text <- hGetContents handle) you will get the output of the headers once the request is finished. Unfortunately, hGetContents doesn't know when to stop waiting for more data before the print statement, because handle is not closed.

    If you additionally restructure the program to have the hClose handle before both the let statement, and print, then the program behaves like you want.

    In the other case, the print is not evaluated because the value of text is never "finalized" by the closing of handle. Since it's "lazy", print is then waiting on hds and lns, which are in turn waiting on text, which is waiting on hClose... which is why you were getting the weird behaviour; hClose was not being evaluated until the socket was needed by the next request, which is why there was no output until then.

    Note that simply making text strict will still block the program forever, leaving it "waiting" for the file to close. Yet, if the file is closed when text is non-strict, it will always be empty, and cause an error. Using both together will get the desired effect.


    Your program with the suggested changes:

    Three changes were made: I added the {-# LANGUAGE BangPatterns #-} pragma, a single character (!) in front of text, and moved hClose handle up a few lines.

    {-# 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
    

    An alternate approach:

    To sidestep issues like this altogether, you can try using the hGetContents function from the System.IO.Strict module instead of System.IO.


    A final note:

    Rather than explicit recursion in acceptLoop, I find the following main to be more idiomatic:

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

    Doing this, you can remove the recursive call from acceptLoop.

    TomMD's solution uses forever from the Contol.Monad module, which is good too.

    0 讨论(0)
  • 2021-01-02 00:48

    You should probably have some notion of when a message is complete. You need to read from the input handle in snippets until you recognize that you've got a complete message. Then assume everything after that is the next message. Messages might not come all at once, or might come in groups.

    Messages might always be a fixed length, for example. Or terminated with \n\n (I believe this is the case for HTTP requests)

    [I may come back and post code to go with this advice, but if I don't, just try and adapt TomMD's code, which is a step in the right direction]

    0 讨论(0)
提交回复
热议问题