Какие силы заставили WAI-приложение быть переработанным пять раз?
Я с любопытством посмотрел на WAI
интерфейс, и хотя он выглядит простым, я был удивлен, увидев, сколько итераций потребовалось для стабилизации в текущей форме!
Я предполагал, что стиль CPS для безопасности ресурсов будет самой интересной вещью, но, похоже, есть чему поучиться!
$ git log -p --reverse -- wai/Network/Wai.hs | grep '\+type Application'
+type Application = Request -> Iteratee B.ByteString IO Response
+type Application = Request -> ResourceT IO Response
+type Application = Request -> C.ResourceT IO Response
+type Application = Request -> IO Response
+type Application = Request -> (forall b. (Response -> IO b) -> IO b)
+type Application = Request -> (Response -> IO ResponseReceived)
-> IO ResponseReceived
Некоторая археология дает несколько неудовлетворительные результаты:
$ git log --reverse -G 'type Application' --pretty=oneline -- wai/Network/Wai.hs | cat
879d4a23047c3585e1cba4cdd7c3e8fc13e17592 Moved everything to wai subfolder
360442ac74f7e79bb0e320110056b3f44e15107c Began moving wai/warp to conduit
af7d1a79cbcada0b18883bcc5e5e19a1cd06ae7b conduit 0.3
fe2032ad4c7435709ed79683acac3b91110bba04 Pass around an InternalState instead of living in ResourceT
63ad533299a0a5bad01a36171d98511fdf8d5821 Application uses bracket pattern
1e1b8c222cce96c3d58cd27318922c318642050d ResponseReceived, to avoid existential issues
1 ответ
Кажется, что все проекты обусловлены тремя основными проблемами:
- У запросов могут быть потоковые тела (поэтому нам не нужно загружать их все в память, прежде чем начинать их обрабатывать). Как лучше всего это представить?
- Ответы также могут быть переданы в потоковом режиме. Как лучше всего это представить?
- Как обеспечить правильное освобождение ресурсов, выделенных на производство ответа? (Например, как обеспечить освобождение дескрипторов файлов после передачи файла?)
type Application = Request -> Iteratee B.ByteString IO Response
В этой версии используются итераторы, которые были ранним решением для потоковой передачи данных в Haskell. Итеративные потребители должны были быть написаны "на основе push", что, возможно, менее естественно, чем "на основе pull", используемые в современных потоковых библиотеках.
Потоковое тело запроса передается итерируемому, и мы получаем Response
значение в конце. Response
содержит перечислитель (функция, которая передает потоковые байты ответа итератору ответа, предоставленному сервером). Предположительно, перечислитель будет управлять распределением ресурсов, используя такие функции, как bracket
,
type Application = Request -> ResourceT IO Response
Эта версия использует монадный преобразователь resourcet для управления ресурсами вместо того, чтобы делать это в перечислителе. Есть специальный Source
введите внутри обоих Request
а также Response
который обрабатывает потоковые данные (и который немного трудно понять, ИМХО).
type Application = Request -> IO Response
Эта версия использует потоковые абстракции из канала, но избегает resourcet и вместо этого предоставляет в виде скобок responseSourceBracket
функция для обработки ресурсов в потоковых ответах.
type Application = Request -> (forall b. (Response -> IO b) -> IO b)
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
Эта версия переходит к подходу на основе продолжения, который позволяет функции обработчика использовать регулярные bracket
-подобные функции для управления распределением ресурсов. Вернемся к исходной точке в этом отношении!
Каналы больше не используются для потоковой передачи. Теперь есть равнина Request -> IO ByteString
функция для чтения фрагментов тела запроса и (Builder -> IO ()) -> IO () -> IO ()
функция в Response
для генерации потока ответов. (The Builder -> IO ()
Функция записи вместе с действием сброса предоставляются сервером.)
Как и в версиях на основе ресурсов, и в отличие от версий на основе итераций, эта реализация позволяет перекрывать чтение тела запроса с потоковой передачей ответа.
Полиморфный обработчик - это хитрый трюк, обеспечивающий ответный обратный вызов Response -> IO b
всегда вызывается: обработчик должен вернуть b
и единственный способ получить его - это вызвать обратный вызов!
Это полиморфное решение, кажется, вызвало некоторые проблемы (возможно, с хранением обработчиков в контейнерах?) Вместо использования полиморфизма, мы можем использовать ResponseReceived
токен без публичного конструктора. Эффект тот же: единственный способ для кода обработчика получить токен, который он должен вернуть, - вызвать обратный вызов.