Pull to refresh

континуации и stateful веб-программирование (Updated!)

Reading time 5 min
Views 2.6K
Идея совсем не нова. Идея древна.
Однако большинство наблюдаемых вокруг веб-фреймворков упорно игнорируют эту идею.

Она заключается в том, чтобы использовать континуации (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.


Будущее веба, однозначно — за языком, поддерживающих полноценные континуации!

Особенно если вспомнить, что в языке, поддерживающим продолжения как объекты первого класса, никакие другие конструкции, по большому счёту, вообще не нужны (включая даже конструкции структурирования данных, инкапсуляции и наследования) — они все реализуются через континуации (и как частный случай — функциональные замыкания))

Ну и, как это обычно бывает, подобные мысли уже высказывались более структурированно и посоледовательно :)

Список неиспользованной литературы:


мега-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, выдёргивается предыдущее состояние и он реально попадает в предыдущую комнату, все пострадавшие при переходе животные оживают, итп.
Tags:
Hubs:
+30
Comments 95
Comments Comments 95

Articles