Розробка continuations-based веб-додатків на Common Lisp з допомогою Weblocks

назад до списку постів

Ця стаття являється перекладом статті Слави Ахмечета http://www.defmacro.org/ramblings/continuations-web.html#do-confirmation

Коли мені доводиться писати заплутаний код для того щоб виразити просту ідею я інстинктивно стараюсь уникнути роботи. Наслідок з цього - те що кожен раз коли я відкриваю поштову скриньку під час кодування, є великий шанс на те що мені потрібна абстракція якої у мене немає.

Я думаю це справджується для всіх програмістів а не тільки до мене. Якщо так, Gmail покладає все своє навантаження на прикладних програмістів. Написання веб-додатків може бути неприємним заняттям через набір проблем (більшість з яких, на мою думку, вирішені фреймворком Seaside, єдина небезпека - те що він примушує людей перестати працювати в Emacs). Три найбільші проблеми які я вирахував це управління даними, управління графічним станом і управління контролем потоку.

Веб-фреймворки зробили великі успіхи у вирішенні половини першої проблеми, ORM-прошарки надають розумне (але не ідеальне) рішення з допомогою автоматичної прив'язки даних до бази даних. На сьогоднішній день жоден фреймворк крім Weblocks з тих що знаю не робить другу половину роботи і автоматично прив'язує модель даних до переносного, доступного ГІ

Кілька фреймворків роблять розумну роботу в управлінні станом ГІ. Найкращий приклад фреймворку з широким профілем застосуванням це - ASP.NET. ViewState і Page_Load жахливі ідеї і через те що делегати це не замикання, написання хорошого ASP.NET компоненту може швидко увігнати вас в жар, залучаючи підтримку важких наркотиків, але вони хоча б направляють в правильний напрямок (каламбурно-орієнтований). UCW та Seaside роблять це чудово. Weblocks це робить. Практично кожен фреймворк базований на компонентах, а не MVC та шаблонних двигунах робить це чудово.

Крім Weblocks, три фреймворки які я знаю роблять хорошу роботу по контролю потоку - Seaside, UCW та PLT. Правильний менеджмент контролю потоку складний для реалізації - він потребує замикання які більшість мов не підтримують. Через те що фреймворки з хорошим контролем управління потоком така рідкість, більшість людей не розуміють що вони втрачають. У цій статті я опишу проблему і те як вона вирішується у Weblocks з використанням continuations (продовжень). Працююча демка що показує практичну реалізацію цих концептів знаходиться тут http://72.249.76.121/ (Хост досить часто не працює - примітка перекладача).

Проблема

Керування контролем потоку у веб-додатках показує своє жахливе лице у двох ситуаціях: модальна взаємодія і послідовності

Модальна взаємодія - будь що передбачає зворотній зв'язок з користувачем перед тим як додаток може працювати далі. Розглянемо підтвердження дії: користувач пробує видалити що-небудь і програма повинна показати йому діалог підтвердження. Можливо просто показати діалог на JavaScript але що буде якщо у клієнта вимкнений JavaScript ? Підходяще рішення показати іншу сторінку (або код через AJAX) на диво складне. Сервер повинен зберегти поточну операцію десь і перенаправити до сторінки підтвердження. Якщо користувач не підтвердить дію, сторінка підтвердження повинна перенаправити на початкову сторінку. Але що буде якщо користувач підтвердить дію ? У менш потужній мові ми можемо симулювати замикання безліччю шаблонів проектування або захардкодити саму потрібну операцію на самій сторінці підтвердження. У більш потужній мові програмування ми можем використовувати замикання, але тоді ми змушені писати програми у continuation-passing стилі. Це було б не так погано в цій ситуації, але стає менш і менш практичним коли залучено послідовності.

Послідовності це набори вкладених модальних взаємодій потенційно з відгалуженнями. Представити набір ліцензійних домовленостей, проходити через процес перевірки кошика покупок, платити своєю кредитною картою в процесі кількох кроків та підтверджувати чийсь розклад це приклади послідовностей. Це і є контроль потоку - частина програми яка робить роботу зробленою. Через те що послідовності можуть бути представлені як серії модальних взаємодій, всі проблеми виведені в минулому абзаці також дійсні для кожного кроку послідовності. Без замикань робити це та зберігати здоровий глузд неможливо. З замиканнями це більш комфортно але не ідеально - поки у Вас не буде трьох широкоформатних моніторів відступи швидко зроблять довгі процеси дуже складними для підтримки.

Елегантне рішення для цих проблем включає використання першокласних continuations якщо вони доступні або перетворення коду у стиль CPS. Через те що Common Lisp не підтримує continuations "з коробки", я написав cl-cont - бібліотеку що перетворює вибрані шматки коду у CPS. Давайте побачим як це працює у Weblocks на практиці.

Обробляєм дії користувача. Перед тим як ми доберемся до continuations, давайте поговорим про те як Weblocks справляється з діяи користувача. Що стається коли користувач натискає на ссилку або кнопку. Weblocks робить це дуже простим - ви просто виводите функції у відповідний html. Наприклад ссилка може показуватись користувачу в такому вигляді:

(render-link (lambda (&rest args)
               (declare (ignore args))
               (do-something))
	     "Some Link")

Лямбда збережена у хеш-таблиці з унікальним ключем. Ссилка з текстом "Some Link" та ссилка-ключ передаються користувачу. Коли користувач клацає на ссилку ключ шукається у хеш-таблиці а функція вибирається і викликається. Через те що ми використовуєм ссилку у цьому випадку ми можемо безпечно ігнорувати аргументи. Якщо ми хочемо відобразити форму, ми повинні використовувати аргументи-ключі щоб отримати доступ до параметрів які були передані для запиту який нас цікавить.

Реалізація render-link робить ссилку асинхроною по замовчування, callback-функція викликається через AJAX. Якщо у клієнта вимкнено JavaScript, використовується звичайний запит. AJAX може бути вимкнений якщо передати :ajaxp nil до render-link

Дії користувача та continuations.

Тепер давайте розглянемо модальну взаємодію. Припустимо "Some Link" веде користувача на контент для якого потрібна автентифікація. Зазвичай нам потрібно пройти через 2-разовий ритуал перенаправлення а потім відкрити Gmail. З правильним контролем потоку ми можем висловити свої наміри прямим та елегантним способом:

(render-link (lambda/cc (&rest args)
               (declare (ignore args))
	       (if (do-page (make-instance 'my-login-widget))
	           (show-protected-content))
                   (show-error))
	     "Some Link")

Ми викликаєм lambda/cc замість lambda для того щоб включити CPS-перетворення для нашої дії. Ми тоді викликаєм do-page з віджетом в якості аргументу. Ось де continuation "сяє". У цій точці continuation зберігається а користувач перенаправляється на нову сторінку та йому показується вікно входу. Коли віджет входу вирішує що користувач може бути автентифікованим, він має викликати відповідь з результатом - функцію що відновить збережену continuation. На цьому етапі do-page поверне значення так, наче це був звичайний виклик функції і генерація початкової сторінки продовжиться. Наш обробник "Some Link" може тоді вирішувати показати захищений контент чи помилку.

Багато може бути написано про lambda/cc та do-page, але це була б стаття про реалізацію CPS-перетворень в Common Lisp та використання delimited continuations для реалізації цікавих речей. Це щось що являється складним. Але коли функціонал на місці, його використання таке ж просте як шматок коду вище.

Weblocks надає ряд примітивів, збудованих на базі continuations. Один з таких примітивів - do-choice - функція що показує модальний діалог з переліком варіантів які може вибрати користувач. На базі do-choice є do-confirmation2 та do-confirmation - функції які дозволяють користувачу підтвердити дію та оглянути інформацію, відповідно.

Перед тим як ми перейдем до послідовностей та процесів давайте поговорим трохи про керування інтерфейсом користувача та базований на віджетах підхід для веб-додатків.

Віджети та керування станом інтерфейсу користувача.

Технологія є обмежуючим фактором ідей. У веб 1.0 правили ASP, C++, J2EE, і деколи, після 6 місяців спроб людей усвідомити що вони до цих пір не мають повноцінний компонент сесій, Perl. Не повинно бути сюрпризом що більшість навдалих Web 1.0 додатків були прославлені корзини покупок - забирало понад два мільйони долларів для написання онлайн-магазину.

Web 2.0 керується MVC. Ми вийшли за рамки корзин покупок. MVC дозволяє Вам збудувати дуже прості додатки дуже швидко. Ідея в тому що прості додатки - це те що хочуть люди. Я підозрюю те що люди справді хочуть крім гидкого каченяти це простий інтерфейс для справді важких процесів що забирають їх час та гроші - щось, що не може бути легко зроблено з MVC у спосіб в який воно завжди використовується.

Так як додатки стають все складнішими, керування станом інтерфейсу користувача стає ще складнішим. На якомусь кроці ми попадаєм у асимптоту складності яку не можна перетнути без перескакування до іншої абстракції. Компоненти (або віджети) надають абстракції що дозволяють перейти через цю невидиму лінію.

Можливо найкращий шлях для показу філософії компонентів на прикладі. Weblocks надає набір віджетів для здійснення загальних операцій. Віджет Dataform дає можливість програмісту видавати дані користувачу, дає користувачу можливість натиснути "Редагувати" щоб побачити форму і відредагувати дані, дозволяє користувачу зберегти зміни або відмінити та повернутися до перегляду даних. Все це у одній стрічці коду.

Інший приклад - таблиця з елементами та автоматичним сортуванням та пагінацією. У Weblocks я можу створити одну в наступний спосіб.

(make-instance 'datagrid :data
               (list some-employee-1
                     some-employee-2
		     some-employee-3)
	       :data-class 'employee)

Найбільша перевага віджетів це те що вони підтримують власний стан. Я можу поставити тисячу віджетів datagrid на сторінці і хай користувач сортує їх різними способами і переходить на інші сторінки. Немає центрального місця яке повинно слідкувати за цим станом - кожен екземпляр віджету відповідає тільки за себе.

Віджети компонуємі. Для прикладу, Віджет Gridedit з Weblocks використовує Datagrid та Dataform щоб дозволити користувачу додавати елементи до таблиціта керувати існуючими. Ця парадигма може бути розширеною на цілий додаток. Composite - віджет який просто включає в себе список інших віджетів (імовірно включаючи інші екземпляри Composite) які будуть відображені в потрібному порядку. Сторінка - це звичайний віджет Composite з іншими віджетами всередині.

Такий підхід дозволяє дуже складні (з точки зору програміста) інтерфейси користувача які можуть бути з легкістю реалізовані будучи легкими у використанні. У фреймворках базованих на компонентах я можу взяти цілу програму та вкласти її у один з моїх віджетів без змін до будь-якої частини програми. Не може бути кращого повторного використання коду ніж у цьому випадку !

Інша перевага - проста підтримка AJAX (фреймворк просто відправляє html оновленого віджету клієнту) яка автоматично відсторонюється від роботи якщо JavaScript вимкнено. Але я відволікаюсь.

Послідовне керування потоком.

Давайте розглянемо простий сценарій потоку - показуємо 3 ліцензійних узгодження в рядок. На кожному кроці користувач має можливість згодитись або відмовитись - якщо користувач згоджується ми переходимо до наступного узгодження. Якщо він відмовляється ми показуємо сторінку з помилкою. Зазвичай кожен крок повинен вміщувати логіку для перенаправлення на наступний крок. У простому сценарії це незручно через те що потік не описаний в одному місці. У більш складному сценарії з гілками підтримка потоку стає кошмаром.

Давайте подивимось як це може бути зроблено у Weblocks:

(with-flow (composite-widgets (root-composite))
  (if (and (yield agreement-1)
           (yield agreement-2)
           (yield agreement-3))
    (show-protected-content)
    (show-error)))

Макрос with-flow перетворюється у шаблонний код необхідний для того щоб зробити трансформації можливими. Перший аргумент макросу - позиція у дереві віджетів в яку потрібно поставити віджети узгодження. У нашому випадку узгодження займуть всю сторінку, корінь дерева. Звідти ми пишем код, використовуючи yield для відображення кожного кроку. Так як і з do-page попереду, кожен віджет з узгодженням повинен викликати відповідь для повернення контролю. У цьому випадку кожен віджет повинен повертати бульове значення. Якщо користувач вирішить не дотримуватись узгодження оператор and поверне значення і ми покажемо помилку. Якщо користувач дасть згоду на всі узгодження, ми покажемо захищений контент.

Зверніть увагу що у будь-який час ми можемо мати кілька потоків у різних частинах додатку (або сторінки). Кожен потік незалежний від інших і може бути збереженим або перезапущеним стільки разів скільки потрібно.

Що далі ?

Оскільки continuations початково складні для розуміння, їх використання дуже просте коли вони обернуті у високорівневі абстракції. Немає сенсу страждати через штучні перенаправлення та спеціальні кінцеві автомати що симулюють керування потоком. Візьміть Seaside, PLT, UCW або Weblocks і починайте !

Якщо у вас є якісь питання коментарі чи пропозиції, будьте ласкаві, накиньте кілька стрічок на coffeemug@gmail.com (Увага, автор не розуміє української, пишіть тільки на англійській - примітка перекладача). Буду радий почути вашу відповідь.