-
Стандартная банковская задача. Предыстория будет отдельным постом. Интересно как можно решить подобную задачу с использованием стат. типизации. Чтобы был контроль компилятором, не надо было полагаться на 100500 тестов, чтобы проверить, что определенные ветки не выполняются и т.п.
Есть модуль X, реализующий API-вызов «изменить сумму заказа».
Заказ
— может быть отправлен или неотправлен
— может быть предоплачен или не предоплачен
— может быть помечен, как несущий риск, или не помечен
(и т.п., см. по ходу пьесы)
Заказ — это сущность, получаемая из базы данных c уже готовыми свойствами. То есть нет последовательности вызовово create_order->risk_check->....->change_sum. Когда-то кем-то где-то был создан заказ. Через две недели мы получили API-вызов, который требует изменить сумму в этом заказе.
Изменение суммы может происходить только по определенным условиям и по определенным правилам, описанных в каждом шаге.
Конечно, в конце концов интересут решение Шага 3. Но интересно увидеть и весь процесс Шаг 1 -> Шаг 2 -> Шаг 3
Шаг 1
— если заказ неотправлен и непредоплачен, сумму можно увеличивать и уменьшать
— если заказ отправлен, сумму нельзя увеличивать, можно уменьшать
— если сумма увеличивается, мы должны провести risk check. если risk check не проходит, товар никак не помечается, но изменение суммы не проходит
— если товар помечен, как risk, то изменять сумму нельзя
Шаг 2
Потом оказалось, что не, так нельзя, поэтому внеслись изменения.
Изменение суммы:
Все то же самое, что и в первой задаче, только:
— увеличивать можно на max(фиксированная сумма1, процент1 от оригинальной суммы заказа)
— уменьшать можно на max(фиксированная сумма2, процент2 от оригинальной суммы заказа),
где сумма1, сумма2, процент1, процент2 — это конфигурация, считываемая из базы данных
Если изменение не попадает в эти рамки, то увеличить/уменьшить нельзя
Шаг 3
И этого оказалось недостаточно
Все то же самое, что и в шаге 2. Дополнительно:
— если заказ предоплачен (неважно, отправлен или нет), можно увеличивать сумму, если это разрешено конфигурацией магазина. Сумма, на которую можно увеличивать высчитывается, как в шаге 2
— если заказ предоплачен, неотправлен, и сумма увеличивается, надо сделать auth-запрос в банк. если он не срабатывает, увеличить нельзя.
— если заказ предоплачен, отправлен, и сумма увеличивается, надо сделать auth-запрос в банк на разницу в сумме, а потом сделать capture запрос на всю сумму. Если хоть один из них не срабатывает, увеличить нельзя.♡ recommended by @qrilka
Replies (66)
-
@dmitriid, предыстория
Disclaimer: Это не срач о том, что статическая типизация круче динамической или наоборот. Это — попытка разобраться.
Присказка:
Год-полтора тому назад на одном широко известном в узких кругах программерском форуме кто-то выложил ссылку на хорошую (без иронии) презентацию Don Stewart Haskell In The Large (можно найти, например, тут dshevchenko.biz) о том, как Хаскель применяется в Standard Chartered Bank.
В обсуждении, естественно, начался пирдуха, обсуждение всего, чего угодно, ну и возникли некоторые вопросы по утверждениям в обсуждении. А так как я как раз тогда работал по сути в банке (https://klarna.com/), а описанная ниже задача в какой-то момент нам съела весь мозг (в частности из-за того, что все надо было сделать на позапрошлой неделе, и ТЗ менялся два раза), я ее привел в укороченном виде.
Вопрос был собственно в том, что многие задачи в банках весьма прямолинейны и тупы: есть некий X, над которым производится N действий в зависимости от M условий. причем условия могут проистекать как из самого X, так и из внешних параметров.
Местами это решается развесистыми FSM'ами (у нас такая тоже была, продвигала dunning chain). Местами — это набор условий, выполяемый по месту (как в описаной задаче).
В общем, было интересно как можно решить подобную задачу с использованием стат. типизации. Чтобы был контроль компилятором, не надо было полагаться на 100500 тестов, чтобы проверить, что определенные ветки не выполняются и т.п.
Задача специально разбита на несколько шагов, потому что в реальном мире в функциональность вносятся изменения, и это надо учитывать при проектировании (то есть мы не можем навесить только один тип, на изменение которого понадобится человеко-год при изменении функциональности)./1 · Reply -
@dmitriid,
Вышеописанное — это, естественно, лишь часть реальности. В реальности все выглядит еще хуже:
----
Условия на запрет изменения
— если заказ помечен как кредит
— если заказ помечен как удаленный
— если заказ помечен как пассивный
— если заказ помечен как замороженный
— если заказ помечен как предоплата
— если заказ помечен как оплаченный по предоплате
— если в заказе нет товаров, которые можно вернуть
— если это аггрегированный заказ с прошедшим сроком оплаты
— если это архивный заказ при условии, что он оплачивается через account
— если сумма увеличивается, а это запрещено настройками заказа
— если сумма увеличивается, а мы уже отослали запрос на оплату в банк клиента
— если сумма увеличивается на сумму большую, чем указано в настройках (относительно оригинальной суммы заказа)
— если сумма увеличивается, а заказ проведен через новую систему
— если мы возвращаем деньги клиенту, заказ находится в одном из трех статусов, не является кредитом, и возвращаемая сумма меньше, чем максимальная разрешенная к возврату сумма
только после этого будет предпринята попытка изменить сумму заказа (отдельный вызов в каком-то отдельном модуле/API).
----
В реальности код написан на Erlang'е в виде цепочки if'ов (case'ов, guard'ов и т.п.), на 100% покрыт юнит-тестами и интеграционными тестами. Последний рефакторинг потребовал работу четверых человек в течение месяца (это, правда, так же включало в себя изменение внешних XML-RPC интерфейсов, выкидывание и переписывание всех тестов и т.п.) -
@qnikst, Не, если отправлен и предоплачен, то изменять нельзя, заканчиваем все проверки.
И да. В шаге 1 нет условия для «заказ не отправлен, но предоплачен». Это мой косяк. Но считаем из того, как записано, что сумму в таком случае можно менять (и надо делать остальные проверки)
если все правила не сработали, то что можно делать с заказом
На практике вызывается сторонний модуль, который собственно меняет сумму. Поэтому в конце выхлоп скорее всего {Заказ, Новая_Сумма} -
@max630,
какая же тут статическая проверка, всё равно в рантайме какая-то ошибка возникнуть должна
Изначально задача возникла из-за невероятно флеймообразующего обсуждения, в котором все били себя пяткой в грудь и заявляли, что все и вся можно проверить стат. типизацией невзирая ни на что :) Сюда я ее переписал, пытаясь не вызывать флеймов :) Как-нибудь напишу выжимку из того, что мне писали :) -
@qnikst, Да, рантайма тут, безусловно, увы (или не увы) много :) Ну я и не требую, чтобы все прямо подряд было стат. типизацией :) Просто интересно, что еще помимо форсирования правильных типов в вызовах функций (что мы вызываем func1(Order, Sum), а не func1(Sum, Order)) можно сделать
-
@dmitriid,
в таком общем виде они скомпилятся успешно, как ни крути, а в рантайме уже будет какое-то исключение.
Да, у меня, в общем, такие же умозаключения. И я совсем не против такого :) Меня просто долго бомбардировали заявлениями типа
Они решают задачу бана неправильного кода, где правильность закодирована в определении. У тебя такого бана нету. Можно проверить is_forbidden и тут же дернуть update_amount, и компилятор и не почешется на эту тему, и останется только уповать на тесты.
как можно больше логики кодировать в типах и проверять компилятором. Чем меньше твой язык позволяет выразить типами, тем меньше у тебя ошибок с типами и тем больше ошибок "в логике", это естественно.
Вот тут как раз и поможет компилятор — как раз с ad-hoc изменениями требований.
Вроде общеизвестно что типы эквивалентны разного рода логическим системам и проектирование софта с использованием строгой типизации (и некоторых других фич фп вроде иммутабельности и "все есть функция") проще и надежнее, чем покрывать тестами каждый несчастный метод с ветвлениями, не дай бог чего пропустили.
А тут как раз подвернулась презентация из банка и задача из банка :)
Но все вышеперечисленное — это призыв к флейму :) -
@dmitriid,
Изначально задача возникла из-за невероятно флеймообразующего обсуждения, в котором все били себя пяткой в грудь и заявляли, что все и вся можно проверить стат. типизацией невзирая ни на что :)
можно конечно, но тут такие проверки (из-за дискуссии тут я таки узнал что в третьей части изменений), хотя это было очевидно, что если описывать все правила на типах, то будет полная программа на типах :]
но вообще конечно можно упростить жизнь, я думаю у меня получится это продемонстрировать. -
@dmitriid, тут есть глобально 2 варианта, которые вижу я (не умеющий в нормальные зависимые типы):
описывать правила декларативно на типах, т.е. что-то вроде
Rule ['NotPayed, 'NotSent] ['Increase, 'Decrease] 'Allow
и потом попросить класс типов написать весь код, на выходе будет действие, которое интерпретатор должен совершить, т.е. что-то вроде
data Result = Ok Order | Deny Reason | Check (m a) (a -> Result)
соотвественно все правила будут помещаться на куске экрана и их легко оценить, да и интерпретатор можно будет видеть, самоме интереное, что наверное даже на основе правил можно будет автоматические тесты написать.
вариант2:
типизировать заказ свойствами и писать правила из минимальных кусокв вида
ruleX :: Order a IsPrepaid b -> Paymend Increase -> Either Error Order a IsPrepaid b
тогда компилятор попросит "доказать" что у тебя нужные типы.
Вариант 1 мне нравится больше.
С другой стороны это можно и без хитрых типов обычной свободной монадой и интепретатором к ней написать, но но без типов это писать было бы чуть печальнее, ну и думать о задаче.
попробую вариант 1 написать -
@dmitriid, здесь нужны собеседники с опытом агды, коков, и прочего ада. Их не то чтобы много, я люблю извращаться с продвинутым typelevel на haskell, чтобы понимать, что можно писать, что нельзя и потом иногда случайно вылезают даже сложные задачи в жизни реальной. В целом большинство кода, который я пишу имеет достаточно простые типы.
С другой стороны даже в такой задаче типы могут быть полезны если хочется сделать какой-то eDSL для того, кто будет писать правила и интерпретатор, просто чтобы интерфейсы не разошлись.
Ну ладно, дальше уже должно работать show me the code, так что вернусь наверное как будет что показать. -
@dmitriid, На коленке меньше, чем за час отвлекаясь на работу получилось что-то такое:
github.com
но на самом деле через пару итераций усложнения это нужно будет просто конвертировать в eDSL :) -
@max630, у меня с ходу просто не получилось :) можно попробовать, я бы наверное с этого начал, а после написания пары правил думал бы что хочу выносить в typelevel.
но как я уже говорил, что более перспективным мне кажется написание eDSL, т.к. там существенно меньше ограничений. Но я бы не решился его писать без системы типов, т.к. я тупой. -
@dmitriid,
Вопрос был собственно в том, что многие задачи в банках весьма прямолинейны и тупы: есть некий X, над которым производится N действий в зависимости от M условий. причем условия могут проистекать как из самого X, так и из внешних параметров.
вот как это решать обобщенно, я не знаю. Точнее догадываюсь, но этот вопрос лежит на границе моего type-level-fu, и подозреваю, что чтобы это понять мне нужно поизучать всякие языки с зависимыми типами и получше понять то над чем goldfire работает в GHC (приближение к зависимым типам, синглтоны, type-in-type и т.д.), тогда может пойму. -
@qnikst, Ой да ладно прибедняться :)
На самом деле пэтому поводу на RSDN, где я задал эту задачу года полтора тому назад, кто-то тоже сказал то же, только другими словами:
Качественных примеров на типах мы в этом треде [на RSDN] вряд ли дождёмся по простой причине: пока что не наработаны методики написания такого кода. Вот, например, когда появилось ООП, то далеко не сразу народ стал активно (а главное — правильно!) его использовать. Нужно подождать. С течением времени появятся новые методики и паттерны, качественная поддержка в компиляторах и IDE, а также опыт разработчиков.
-
@dmitriid, мне можно :) индустрия обычно отсает от CS лет на 15-20, я более чем уверен, что за последние 20 лет было написано достаточно работ, которые применимы в тут. Поскольку я позиционирую себя, как человека, работающего на языке на bleeding edge, то я должен быть в курсе последних работ и тенденций, и если и не понимать их, то хотя бы знать куда копать и отсылаться. В данном случае я не знаю