Haskell лень - как я могу заставить IO случиться раньше?

Я только начал изучать 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

спасибо, Rob

Followup

Все ответы были полезными. Приведенный ниже код работает, но пока не использует bytestrings, как это было предложено. Следующий вопрос: можно ли заменить 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

Ответы

Ответ 1

Ваша проблема в использовании hGetContents получит все содержимое на ручке до закрытия сокета. Вы следуете этому вызову, пытаясь проанализировать последнюю строку ввода, которая не будет известна до тех пор, пока соединение не завершится.

Решение: получите столько данных, сколько вам нужно (или доступно), а затем завершите соединение.

Поздно, и я устал, но здесь решение, которое я знаю, неоптимально (читай: уродливый как грех): вы можете перейти к bytestrings (должен делать это в любом случае) и использовать hGetNonBlocking или hGetSome вместо 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

Ответ 2

Краткий обзор подхода:

"Контроль потока" в ленивых программах отличается от того, к которому вы привыкли. Вещи не будут оцениваться до тех пор, пока у них не будет, поэтому ваша программа всегда запрашивает результат с выходом.

В общем, вы можете сделать что-то строгое, используя оператор "bang" ! и прагма BangPatterns.

Если вы используете его в этом случае (говоря !text <- hGetContents handle), вы получите результат заголовков после завершения запроса. К сожалению, hGetContents не знает, когда прекратить ждать дополнительных данных перед оператором print, потому что handle не закрыт.

Если вы дополнительно реструктурируете программу, чтобы иметь hClose handle перед оператором let и print, тогда программа ведет себя так, как вы хотите.

В другом случае print не оценивается, потому что значение text никогда не завершается закрытием handle. Поскольку он "ленив", print затем ожидает hds и lns, которые в очереди ждут на text, который ждет на hClose... вот почему вы получали странное поведение; hClose не оценивался до тех пор, пока сокет не был нужен для следующего запроса, поэтому до этого не было вывода.

Обратите внимание, что просто создание text strict будет по-прежнему блокировать программу навсегда, оставив "ожидание" для закрытия файла. Тем не менее, если файл закрыт, когда 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

Альтернативный подход:

Чтобы обойти такие проблемы, вы можете попробовать использовать функцию hGetContents из System.IO.Strict вместо System.IO.


Последнее примечание:

Вместо явной рекурсии в acceptLoop я нахожу следующее main более идиоматичным:

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

Выполняя это, вы можете удалить рекурсивный вызов из acceptLoop.

Решение TomMD использует forever из модуля Contol.Monad, что тоже хорошо.

Ответ 3

Вероятно, у вас должно быть некоторое представление о завершении сообщения. Вы должны прочитать из дескриптора ввода в фрагментах, пока не узнаете, что у вас есть полное сообщение. Затем предположим, что все это будет следующим сообщением. Сообщения могут не появляться сразу или могут появляться в группах.

Сообщения могут всегда быть фиксированной длиной, например. Или завершено с помощью \n\n (я считаю, что это относится к HTTP-запросам)

[Я могу вернуться и отправить код, чтобы пойти с этим советом, но если я этого не сделаю, просто попробуйте и скопируйте TomMD-код, который является шагом в правильном направлении]