Идея совсем не нова. Идея древна.
Однако большинство наблюдаемых вокруг веб-фреймворков упорно игнорируют эту идею.
Она заключается в том, чтобы использовать континуации (continuations) для магического превращения RESTful (stateless) веб-приложений в более удобный и привычный stateful формат.
По сути, «сессия» придумана, чтобы хранить состояние выполнения веб-приложения.
А использование её тупо как набора данных — исключительно из-за отсутсвия в языке полнофункциональных продолжений.
Континуация же — это состояние выполнения приложения на уровне языка.
В scheme он поддерживаются как объекты первого класса — их можно возвращать из функций, передавать как параметры функций, и наконец — вызывать как функции.
При вызове континуации, приложение возвращается в то место, где продолжение было создано, и продложает выполняться оттуда.
Причём это можно делать не один раз.
В некотором роде — это исключения, но действующие наоборот.
Исключение возникает в какой-то точке программы и передаёт управление кудато вовне — там где установлен обработчик.
Продолжение возникает в какой-то точке программы и позволяет передать управление обратно в эту точку. Программа в этом месте будет считать что так и было.
Например, в питоне это реализовано оператором yield.
Функция, содержащая yield считается «генератором итераторов», её выполнение прекращается на первом операторе yield, а функция возвращает объект типа итератор, к которому можно применять next() для вызова следующей итерации, и send(значения) для возврата значения в точку перехода по yield.
Вызов итератора методом next() и send() возвращает очередное, следующее, значение.
Итераторы можно использовать для итерирования чисел (и это может быть безумно весело, если числа, скажем — фиббоначи, и их — бесконечное количество), можно делать обход дерева, можно итерировать всё и вся.
Точно также итератор может генерить не числа или узлы дерева, а web-страницы.
Например, функции для просмотра ресурсов и засовывания их в корзину покупок могут выглядеть так:
Параметр continuation — это состояние приложения (выполнения другой функции) в том месте где был совершён переход по ссылке «купить хрень».
Основное приложение при таком раскладе выглядит как итератор.
cont = browse_stuff_iter()
Объект cont сохраняется в сессии, или где-нибудь ещё, и восстанавливается при каждом обращении клиента.
Или же сохраняется несколько объектов, и их идентификаторы кодируются в урлах, засунутых в скрытые поля форм, так, что при нажажатии кнопки «back» пользователь не просто видит предыдущую страницу, а реально возвращается в предыдущее состояние приложения.
На запрос GET вызывается cont.next()
На запрос POST вызывается cont.send(данные формы)
Результаты этих вызовов отображаются как страницы.
Такой метод позволяет генерить цепочку форм как в buy_stuff()
При переходе по новой ссылке, это расценивается как прерывание и текущее состояние передаётся обработчику: buy_stuff(cont,item)
Будущее веба, однозначно — за языком, поддерживающих полноценные континуации!
Особенно если вспомнить, что в языке, поддерживающим продолжения как объекты первого класса, никакие другие конструкции, по большому счёту, вообще не нужны (включая даже конструкции структурирования данных, инкапсуляции и наследования) — они все реализуются через континуации (и как частный случай — функциональные замыкания))
Ну и, как это обычно бывает, подобные мысли уже высказывались более структурированно и посоледовательно :)
Список неиспользованной литературы:
Однако большинство наблюдаемых вокруг веб-фреймворков упорно игнорируют эту идею.
Она заключается в том, чтобы использовать континуации (continuations) для магического превращения RESTful (stateless) веб-приложений в более удобный и привычный stateful формат.
По сути, «сессия» придумана, чтобы хранить состояние выполнения веб-приложения.
А использование её тупо как набора данных — исключительно из-за отсутсвия в языке полнофункциональных продолжений.
Континуация же — это состояние выполнения приложения на уровне языка.
В scheme он поддерживаются как объекты первого класса — их можно возвращать из функций, передавать как параметры функций, и наконец — вызывать как функции.
При вызове континуации, приложение возвращается в то место, где продолжение было создано, и продложает выполняться оттуда.
Причём это можно делать не один раз.
В некотором роде — это исключения, но действующие наоборот.
Исключение возникает в какой-то точке программы и передаёт управление кудато вовне — там где установлен обработчик.
Продолжение возникает в какой-то точке программы и позволяет передать управление обратно в эту точку. Программа в этом месте будет считать что так и было.
Например, в питоне это реализовано оператором yield.
Функция, содержащая yield считается «генератором итераторов», её выполнение прекращается на первом операторе yield, а функция возвращает объект типа итератор, к которому можно применять next() для вызова следующей итерации, и send(значения) для возврата значения в точку перехода по yield.
Вызов итератора методом next() и send() возвращает очередное, следующее, значение.
Итераторы можно использовать для итерирования чисел (и это может быть безумно весело, если числа, скажем — фиббоначи, и их — бесконечное количество), можно делать обход дерева, можно итерировать всё и вся.
Точно также итератор может генерить не числа или узлы дерева, а web-страницы.
Например, функции для просмотра ресурсов и засовывания их в корзину покупок могут выглядеть так:
browse_stuff():
# вывести в браузер форму ввода критериев поиска и сохранить введённое в объект
criteria = yield(ask_criteria())
# поиск ресурсов по критериям
results = find_stuff(criteria)
# вывести в браузер список ресурсов, листаемый по страницам и чего-нибыдь там ещё
yield (list_stuff(results))
# прекратить это гнусное дело
raise StopIteration()
def buy_stuff(continuation, item)
if not user.authentificated:
# переход на страницу запроса пароля, восстановления логина, регистрации
user = yield(login_form(user))
# теперь пользователь залогинен, либо создан новый, и можно продолжать
# спросить, сколько
quantity = yield(ask_quantity())
# спросить, куда доставлять
delivery = yield(ask_delivery())
# попросить денег
money = yield(ask_payment())
# оформить заказ
status = launch_order(item,quantity,delivery,money)
# показать страницу с кнопочкой "вернуться"
yield(message("спасибо за покупку. ждите курьера."))
# вернуться туда, где была вызвана функция (например в browse_stuff)
continuation.next()
# интересно, оптимизирует ли питон такую "хвостовую рекурсию" ?
Параметр continuation — это состояние приложения (выполнения другой функции) в том месте где был совершён переход по ссылке «купить хрень».
Основное приложение при таком раскладе выглядит как итератор.
cont = browse_stuff_iter()
Объект cont сохраняется в сессии, или где-нибудь ещё, и восстанавливается при каждом обращении клиента.
Или же сохраняется несколько объектов, и их идентификаторы кодируются в урлах, засунутых в скрытые поля форм, так, что при нажажатии кнопки «back» пользователь не просто видит предыдущую страницу, а реально возвращается в предыдущее состояние приложения.
На запрос GET вызывается cont.next()
На запрос POST вызывается cont.send(данные формы)
Результаты этих вызовов отображаются как страницы.
Такой метод позволяет генерить цепочку форм как в buy_stuff()
При переходе по новой ссылке, это расценивается как прерывание и текущее состояние передаётся обработчику: buy_stuff(cont,item)
Upd:
Стоит заметить, что ни в питоне, ни темболее в пхп, ни в жаве, нет полноценной поддержки продолжений, на уровне объектов первого класса.
Питоновский yield ведёт себя очень похоже и был использован для иллюстрации идеи.
Как оно будет работать в действителности — не совсем понятно.
Полная поддержка континуаций есть в разных экзотических языках, но и в Ruby в том числе.
Существует несколько серверов с поддержкой продолжений для scheme, lisp, smalltalk, OCaml и JavaScript.
Где-то в комментах затерялась ссылка на простой эмулятор продложений для PHP.
Будущее веба, однозначно — за языком, поддерживающих полноценные континуации!
Особенно если вспомнить, что в языке, поддерживающим продолжения как объекты первого класса, никакие другие конструкции, по большому счёту, вообще не нужны (включая даже конструкции структурирования данных, инкапсуляции и наследования) — они все реализуются через континуации (и как частный случай — функциональные замыкания))
Ну и, как это обычно бывает, подобные мысли уже высказывались более структурированно и посоледовательно :)
Список неиспользованной литературы:
- Пересекая границы: Continuation, Web-разработка и Java-программирование
- Inverting back the inversion of control or, Continuations versus page-centric programming.
мега-UPD: иллюстрация на scheme
Не ручаюсь за правильность кода. но скобки сбалансированы :)
;; server code (define (uri->cont uri) "выуживает откуда-то продолжение по его URI" ) (define (cont->uri cont) "сохраняет куда-то продолжение и формирует для него URI ) (define (insert-uri tmpl uri) "вставляет ссылку в шаблон" ) (define (render-page tmpl args) "рендерит страницу в html" ) (define (handle-request request) "вызывает продолжение, передавая ему request" (if (eqv? (get-uri request) init-uri)) ; если запрос на стартовый URI (start) ; вызвать старт ((uri->cont (get-uri request)) request))) ; иначе - продолжение (define (make-response cont template) "template: страница со ссылками или форма." (render-page (insert-uri template (cont->uri cont)))) ;; end of server code ;; application code (define (quest room) (define (parse-request request) "парсит данные формы или чонить и возвращает choice" ) (define (walk-on choice) "выбрает новую комнату по текущей и choice" ) (define (get-page) "возвращает html-шаблон для текущей комнаты" ) (define (response cc) (make-response cc (get-page)) "тупо карринг параметра" ) (quest (walk-on (parse-request (call/cc response))))) ;; initialization code (define init-uri "/mytextquest") (define (start) (quest 'start-room))
Что тут происходит (должно происходить):
0. quest вызывается с парамтером идентифицирующим комнату.
0.5 единственный вызов — последний, самый крайний параметр — выражение (call/cc (response))
1. call/cc (этотакая специальная конструкция, которая) вызывает функцию (response continuation)
2. та в свою очередь — вызывает (make-response continuation template)
3. make-response вставляет в шаблон (например в поле action, или в ссылки перехода) URI идентифицирующий это продолжение.
4. страница рендерится
5. юзер кликает по одной из ссылок на странице или субмитит форму
6. handle-request выуживает по ссылке продолжение
7. и вызывает его с параметром request
8. выполнение продолжается с того места, где стоит call/cc. значение request подставляется в качестве результата вызова call/cc
9. request парсится и мы получаем логику чего там тыкнул юзер
10. walk-on вычисляет в какую комнату попадёт теперь юзер
11. quest рекурсивно вызывается с параметром, идентифицирующим следующую комнату
Если кроме комнаты на выбор пути влияет карма юзера. содержимое карманов. и состояние окружающей экологии — всё это инкапсулируется в 'room'.
Если юзер нажал 'back' и перешёл на предыдущий URI, выдёргивается предыдущее состояние и он реально попадает в предыдущую комнату, все пострадавшие при переходе животные оживают, итп.