• баттл банкизло кодмазафакадуюспикит статика динамика *erlang *haskell *ктонановенького

    Стандартная банковская задача. Предыстория будет отдельным постом. Интересно как можно решить подобную задачу с использованием стат. типизации. Чтобы был контроль компилятором, не надо было полагаться на 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 тестов, чтобы проверить, что определенные ветки не выполняются и т.п.

    Задача специально разбита на несколько шагов, потому что в реальном мире в функциональность вносятся изменения, и это надо учитывать при проектировании (то есть мы не можем навесить только один тип, на изменение которого понадобится человеко-год при изменении функциональности).
  • @dmitriid,
    Вышеописанное — это, естественно, лишь часть реальности. В реальности все выглядит еще хуже:

    ----

    Условия на запрет изменения

    — если заказ помечен как кредит
    — если заказ помечен как удаленный
    — если заказ помечен как пассивный
    — если заказ помечен как замороженный
    — если заказ помечен как предоплата
    — если заказ помечен как оплаченный по предоплате
    — если в заказе нет товаров, которые можно вернуть

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

    — если сумма увеличивается, а заказ проведен через новую систему
    — если мы возвращаем деньги клиенту, заказ находится в одном из трех статусов, не является кредитом, и возвращаемая сумма меньше, чем максимальная разрешенная к возврату сумма

    только после этого будет предпринята попытка изменить сумму заказа (отдельный вызов в каком-то отдельном модуле/API).

    ----

    В реальности код написан на Erlang'е в виде цепочки if'ов (case'ов, guard'ов и т.п.), на 100% покрыт юнит-тестами и интеграционными тестами. Последний рефакторинг потребовал работу четверых человек в течение месяца (это, правда, так же включало в себя изменение внешних XML-RPC интерфейсов, выкидывание и переписывание всех тестов и т.п.)
  • @dmitriid, тэг спецолимпиада?
  • @qrilka, черт забыл
  • @dmitriid, добавь
  • @dmitriid,
    1. как?
    2. там и так не все поместились :)
  • @dmitriid, оукэй
  • @dmitriid, ну и линку на эрланговариант запость для полноты картины чтоль
  • @dmitriid, Поясни сразу правила как объединяются, вот на первом шаге, если срабатывает первое правило, то нужно ли проверять четвертое например?
  • @qrilka, у меня только псевдокод есть. сам код NDA естественно :)
  • @dmitriid, а вообще ты левые по сути тэги запихал вместо важных :)
  • @qnikst, Ну, там выход из функции типа (нельзя_увеличивать && нельзя_уменьшать). Если только нельзя_уменьшать, то надо идти дальше по условиям
  • @dmitriid, не знал, что есть ограничения на тэги :)
  • @dmitriid, ну тут по своему огорожено :)
  • @qnikst, но в целом можно комбинировать, как угодно, лишь бы правильно было :) На практике risk check и auth/capture будут скинуты к самому концу проверок, потому что они потенциально стоят денег. но в данный момент это не так важно
  • @qrilka, удалить и запостить по новой? :)
  • @dmitriid, ну смотри, первое правило:
    1. если не отправлен и не предоплачен, то можно увеличивать и уменьшать

    вопрос в том, нужно ли мне проверять дальшейшие ограничения? если нужно, то нафига это правило существует.
  • @dmitriid, проще ещё один пост с правильными тегами и ссылку на этот в нём
  • @qnikst, А, проверять дальше, конечно. Правило существует, чтобы сразу отсечь уже заказы, которые (отправленн && предоплачен)
  • @dmitriid, не понимаю.. т.е. это правило одновремено обозначает наличие правила: "если отправлен или предоплачен то изменять нельзя"? т.е. для каждого правила есть вариант, который "проотрицали"?
  • @qnikst, примерно сюда же вопрос, если все правила не сработали, то что можно делать с заказом
  • @dmitriid, a "можно" это вообще что предполагается? Если у тебя интерфейс берёт заказ из гомогенного мешка, и применяет запрос из API, какая же тут статическая проверка, всё равно в рантайме какая-то ошибка возникнуть должна
  • @qnikst, Не, если отправлен и предоплачен, то изменять нельзя, заканчиваем все проверки.

    И да. В шаге 1 нет условия для «заказ не отправлен, но предоплачен». Это мой косяк. Но считаем из того, как записано, что сумму в таком случае можно менять (и надо делать остальные проверки)

    если все правила не сработали, то что можно делать с заказом

    На практике вызывается сторонний модуль, который собственно меняет сумму. Поэтому в конце выхлоп скорее всего {Заказ, Новая_Сумма}
  • @max630, «Можно» значит разрешено бизнесом :) То есть в данном случае

    — если условие «увеличивать можно на max(фиксированная сумма1, процент1 от оригинальной суммы заказа)»
    — API-запрос пытается увеличить на сумму больше max(…)
    — то запрещаем увеличение
  • @max630, ну тут в любом случае будет рантайма много, но с типами можно интересные штуки сделать, минимум попросить компилятор сделать все проверки самому. В общем если опять в 11 работать не закончу, то попробую сделать все
  • @max630,

    какая же тут статическая проверка, всё равно в рантайме какая-то ошибка возникнуть должна

    Изначально задача возникла из-за невероятно флеймообразующего обсуждения, в котором все били себя пяткой в грудь и заявляли, что все и вся можно проверить стат. типизацией невзирая ни на что :) Сюда я ее переписал, пытаясь не вызывать флеймов :) Как-нибудь напишу выжимку из того, что мне писали :)
  • @dmitriid, ну я к тому что есть функция взятия запроса из базы чегото -> (какая-то монада) Запрос, есть функция увеличения Увеличить Запрос Сумма -> чего-то, в таком общем виде они скомпилятся успешно, как ни крути, а в рантайме уже будет какое-то исключение.
  • @qnikst, Да, рантайма тут, безусловно, увы (или не увы) много :) Ну я и не требую, чтобы все прямо подряд было стат. типизацией :) Просто интересно, что еще помимо форсирования правильных типов в вызовах функций (что мы вызываем func1(Order, Sum), а не func1(Sum, Order)) можно сделать
  • @dmitriid,
    в таком общем виде они скомпилятся успешно, как ни крути, а в рантайме уже будет какое-то исключение.

    Да, у меня, в общем, такие же умозаключения. И я совсем не против такого :) Меня просто долго бомбардировали заявлениями типа

    Они решают задачу бана неправильного кода, где правильность закодирована в определении. У тебя такого бана нету. Можно проверить is_forbidden и тут же дернуть update_amount, и компилятор и не почешется на эту тему, и останется только уповать на тесты.

    как можно больше логики кодировать в типах и проверять компилятором. Чем меньше твой язык позволяет выразить типами, тем меньше у тебя ошибок с типами и тем больше ошибок "в логике", это естественно.

    Вот тут как раз и поможет компилятор — как раз с ad-hoc изменениями требований.

    Вроде общеизвестно что типы эквивалентны разного рода логическим системам и проектирование софта с использованием строгой типизации (и некоторых других фич фп вроде иммутабельности и "все есть функция") проще и надежнее, чем покрывать тестами каждый несчастный метод с ветвлениями, не дай бог чего пропустили.

    А тут как раз подвернулась презентация из банка и задача из банка :)

    Но все вышеперечисленное — это призыв к флейму :)
  • @dmitriid,
    Изначально задача возникла из-за невероятно флеймообразующего обсуждения, в котором все били себя пяткой в грудь и заявляли, что все и вся можно проверить стат. типизацией невзирая ни на что :)

    можно конечно, но тут такие проверки (из-за дискуссии тут я таки узнал что в третьей части изменений), хотя это было очевидно, что если описывать все правила на типах, то будет полная программа на типах :]

    но вообще конечно можно упростить жизнь, я думаю у меня получится это продемонстрировать.
  • @qnikst, Вот это было бы очень интересно (без иронии и подколок)!!
  • @qnikst,

    если описывать все правила на типах, то будет полная программа на типах :]

    Это, кстати, другой интересный момент — когда вся программа на типах, кто и как проверяет корректность этой программы :) Но, думаю, это отдельное большое обсуждение :)
  • @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 написать
  • @qnikst, brain. blown. :)
  • @dmitriid, здесь нужны собеседники с опытом агды, коков, и прочего ада. Их не то чтобы много, я люблю извращаться с продвинутым typelevel на haskell, чтобы понимать, что можно писать, что нельзя и потом иногда случайно вылезают даже сложные задачи в жизни реальной. В целом большинство кода, который я пишу имеет достаточно простые типы.
    С другой стороны даже в такой задаче типы могут быть полезны если хочется сделать какой-то eDSL для того, кто будет писать правила и интерпретатор, просто чтобы интерфейсы не разошлись.
    Ну ладно, дальше уже должно работать show me the code, так что вернусь наверное как будет что показать.
  • @qnikst, жаль, что только один волонтёр пока :)
  • @qrilka, интересно бы @rufuse послушать, я так понимаю о них похожие задачи возникают
  • @dmitriid,
    как можно решить подобную задачу с использованием стат. типизации
    Никак. Статическая типизация проверяет, что код выполняется. Саму логику кода проверяет статический анализ.
  • @rkit, Спасибо!
  • @dmitriid, Честно говоря нет опыта, описания таких спецификаций на типах, но есть мнение, что вы сможете как-нибудь сэкономить на юнит-тестах, заменив часть из них типами, интеграционные же как были, так и останутся.
  • @dmitriid, На коленке меньше, чем за час отвлекаясь на работу получилось что-то такое:

    github.com

    но на самом деле через пару итераций усложнения это нужно будет просто конвертировать в eDSL :)
  • @qnikst, соотвественно по файликам можно проследить как что менялось, делать честную историю мне было лень
  • @qnikst, а почему sum? :)
  • @qrilka, Не помню, на rsdn про сумму заказа было, а писать order лень было
  • @qnikst, Ого! Ого!!!! Спасибо!! Буду смотреть!
  • @nyuufac, Вот как раз собственно в том, как и что описывать типами ;) Тут выше(ниже?) интересное предложили: juick.com
  • @qnikst, АААА Почитал наискосок, это прекрасно. Спасибо!!!!
  • @qnikst, Саш, можэт это на рухачкель стоит оформить?
  • @qnikst, а теперь бы хорошо кто-нибудь объяснил, чем это лучше чем сделать то же самое в рантайме обычным ADT и паттерн-матчингом, а не в типах.
  • @max630, нарисуй АДТ и паттерн-матчинг чтоб сравнивать-то?
  • @qrilka, что-то подобное: rsdn.ru
  • @qrilka, Надо бы, на выходных попробую
  • @dmitriid, ну откудаж в ырланке АДТ :)
  • @qnikst, свисти еси чо, с вычиткой могу попробовать помочь
  • @max630, у меня с ходу просто не получилось :) можно попробовать, я бы наверное с этого начал, а после написания пары правил думал бы что хочу выносить в typelevel.

    но как я уже говорил, что более перспективным мне кажется написание eDSL, т.к. там существенно меньше ограничений. Но я бы не решился его писать без системы типов, т.к. я тупой.
  • @qnikst, имелось ввиду, что на работе я бы с этого начал.
  • @dmitriid,
    Вопрос был собственно в том, что многие задачи в банках весьма прямолинейны и тупы: есть некий X, над которым производится N действий в зависимости от M условий. причем условия могут проистекать как из самого X, так и из внешних параметров.

    вот как это решать обобщенно, я не знаю. Точнее догадываюсь, но этот вопрос лежит на границе моего type-level-fu, и подозреваю, что чтобы это понять мне нужно поизучать всякие языки с зависимыми типами и получше понять то над чем goldfire работает в GHC (приближение к зависимым типам, синглтоны, type-in-type и т.д.), тогда может пойму.
  • @qnikst, Ой да ладно прибедняться :)

    На самом деле пэтому поводу на RSDN, где я задал эту задачу года полтора тому назад, кто-то тоже сказал то же, только другими словами:

    Качественных примеров на типах мы в этом треде [на RSDN] вряд ли дождёмся по простой причине: пока что не наработаны методики написания такого кода. Вот, например, когда появилось ООП, то далеко не сразу народ стал активно (а главное — правильно!) его использовать. Нужно подождать. С течением времени появятся новые методики и паттерны, качественная поддержка в компиляторах и IDE, а также опыт разработчиков.
  • @qnikst, хороший, годный hasochism
  • @qnikst, Денис жуёт там свои дурианы пока, кинуть ссылку в бананы и линзы, может и Роман бы ещё чего высказал в оч. выпуске подкаста
  • @dmitriid, мне можно :) индустрия обычно отсает от CS лет на 15-20, я более чем уверен, что за последние 20 лет было написано достаточно работ, которые применимы в тут. Поскольку я позиционирую себя, как человека, работающего на языке на bleeding edge, то я должен быть в курсе последних работ и тенденций, и если и не понимать их, то хотя бы знать куда копать и отсылаться. В данном случае я не знаю
  • @qnikst, мне просто кажется что код на типах практически дословно воспроизводит код на значениях. В чём тогда профит? Может даже наоборот, код на типах окажется хуже типизирован. Но это я уже фантазирую.

    сам поковыряться в этом я не раньше выходных смогу
  • @qrilka, Опоздали на час, я сегодня работаю активно и на выпуск не пошёл. Можно в следующий раз попробовать :)
  • @max630, код на типах практически дословно воспроизводит спецификацию заданную словами, код на значениях тоже воспроизводит спецификацию на словах. Скорее всего код на типах и код на значениях будут похожи, и мало что с этим сделаешь.
  • @qnikst, Рома на тебя стрелки перевёл — disqus.com
  • @qrilka, на следующей неделе зайду