Комментарии 28
Пишу сейчас процессинг. Море операций с файловой системой, базой данных, сторонними сервисами и программами. Пришел к похожей схеме с различием в структуре данных.
Для себя остановился на funcool/cats и монаде Either.
PS: Ваш блок let
функции buy-lot
заставляет моё сердце кровоточить :)
0
Что именно заставляет «сердце кровоточить», интересно? :)
0
Многословность. В моем понимании в let
блок должен быть максимально лаконичным.
with-lot-fn
, buy-lot-fn
вынести наружу. buy-lot-fn
разбить для более легкого понимания.
Лично меня начинает клинить если в функции большая вложенность.
Вложенные let
блоки причиняют страдания.
Всё это разумеется ИМХО
0
В данном случае функции `with-lot-fn` и `buy-lot-fn` можно делать как приватными внутри функции `buy-lot` (в этом случае им доступны параметры функции `buy-lot` без прокидывания их через контекст), так и вынести их наружу (и тогда все данные нужно брать из контекста). Плюс, здесь нужно учитывать, что если функцию для каждого шага прописывать в неймспейсе, то у вас в этом неймспейсе получится адское количество таких функций, большинство которых будут использоватся только в одном месте.
Все остальное — дело вкуса, цвета и запаха :).
Все остальное — дело вкуса, цвета и запаха :).
0
Посмотрите на Clojure пристально. Там наверняка есть монады.
В ФП контекст обеспечивает монада State.
В ФП контекст обеспечивает монада State.
0
Что именно в программировании с монадами будет проще/лучше, чем в контекстном программировании?
Наверное, не все, что существует нужно применять потому, что это существует => хотелось бы узнать, чем программировании с монадами проще/лучше :)
Наверное, не все, что существует нужно применять потому, что это существует => хотелось бы узнать, чем программировании с монадами проще/лучше :)
0
Конкретно в данном случае — вы используете монаду Either для ошибок и State для контекста. Чистая функция, которая может вернуть результат или ошибку — пишется именно с помощью Either. А until-first-error — это bind для Either (точнее, часть с матчингом) плюс костыль — обработка списка, которая решается функцией fold.
Проще говоря, вы не знаете, что монады уже умеют всё это делать, и поэтому городите велосипеды.
Плюс, если посмотреть на типичные монады Reader — Writer — State — они предназначены для ситуаций только читать(конфиги) — только писать(логи) — читать+писать(контекст). И чтобы использовать любую из них, достаточно завернуть функцию в монаду. А вам, чтобы сделать Reader+State, например, придется либо оставить конфиги изменяемыми и сделать их частью контекста(а потом искать ошибку, когда кто-то по ошибке их поменяет), либо городить еще больше велосипедов.
Проще говоря, вы не знаете, что монады уже умеют всё это делать, и поэтому городите велосипеды.
Плюс, если посмотреть на типичные монады Reader — Writer — State — они предназначены для ситуаций только читать(конфиги) — только писать(логи) — читать+писать(контекст). И чтобы использовать любую из них, достаточно завернуть функцию в монаду. А вам, чтобы сделать Reader+State, например, придется либо оставить конфиги изменяемыми и сделать их частью контекста(а потом искать ошибку, когда кто-то по ошибке их поменяет), либо городить еще больше велосипедов.
+4
Сравнить контекстное программирование с программированием с монадами — действительно интересный и важный вопрос.
Любому человеку, незнакомому с контекстным программированием и монадным программированием из того, что вы написали понятно следующее:
— в контекстном программировании вводится понятие результата элементарного действия [:ok updated_context] | [:error reason] и, если это соответствует задаче, предлагается разбить логику на элементарные действия и использовать функцию `until-first-error`;
— в монадном программировании вводятся сущности: монада, типы монад (Maybe, Either, State, Reader, Writer и др.). И дальше предлагается изучить как это все готовить.
Возможно, что эта сложность монад, по мере роста масштаба приложения, в некоторый момент себя оправдает. Но совершенно очевидно, что для приложений среднего масштаба это сложность является излишней.
Ваш пример с Reader+State я пока не понял (в чем там killing feature у монад перед контекстным программированием), но еще подумаю.
Любому человеку, незнакомому с контекстным программированием и монадным программированием из того, что вы написали понятно следующее:
— в контекстном программировании вводится понятие результата элементарного действия [:ok updated_context] | [:error reason] и, если это соответствует задаче, предлагается разбить логику на элементарные действия и использовать функцию `until-first-error`;
— в монадном программировании вводятся сущности: монада, типы монад (Maybe, Either, State, Reader, Writer и др.). И дальше предлагается изучить как это все готовить.
Возможно, что эта сложность монад, по мере роста масштаба приложения, в некоторый момент себя оправдает. Но совершенно очевидно, что для приложений среднего масштаба это сложность является излишней.
Ваш пример с Reader+State я пока не понял (в чем там killing feature у монад перед контекстным программированием), но еще подумаю.
0
На самом деле большинство людей использует монады задолго до того, как слышит слово монада. Те же самые Maybe (т.е. вернуть значение или ничего) и Either (вернуть значение или ошибку) спокойно используются без каких-либо знаний о монадах.
И как раз-таки ваше «элементарное действие» это просто функция, принимающая дополнительным параметром Context и возвращающая Either Error Context. Даже монады знать не надо, чтобы так написать.
А потом, когда вы хотите скомбинировать несколько «действий» в одно, которое бы вернуло итоговый результат или первую ошибку — вы можете написать комбинирующую функцию вручную, а можете воспользоваться тем, что Either это монада и значит её можно комбинировать с другими Either — как раз так, как вам надо. И о монадах можете продолжать не знать ничего, кроме как что они комбинируются между собой.
Плюс заметьте, что мы совсем даже не использовали State — потому что возвращаем новый контекст в результате. А что если мы хотим вернуть какой-то осмысленный результат? Конечно, вы можете возвращать пару из Context и результата. Но как потом скомбинировать это действие с другим действием, которое принимает как аргументы ваш результат и Context(не как одну пару)? Ещё больше костылей. Или же вы прочитаете про монаду State и используете её.
TL DR: вы уже написали код с монадой Either, но не знаете об этом — и поэтому не пользуетесь преимуществами монад.
И как раз-таки ваше «элементарное действие» это просто функция, принимающая дополнительным параметром Context и возвращающая Either Error Context. Даже монады знать не надо, чтобы так написать.
А потом, когда вы хотите скомбинировать несколько «действий» в одно, которое бы вернуло итоговый результат или первую ошибку — вы можете написать комбинирующую функцию вручную, а можете воспользоваться тем, что Either это монада и значит её можно комбинировать с другими Either — как раз так, как вам надо. И о монадах можете продолжать не знать ничего, кроме как что они комбинируются между собой.
Плюс заметьте, что мы совсем даже не использовали State — потому что возвращаем новый контекст в результате. А что если мы хотим вернуть какой-то осмысленный результат? Конечно, вы можете возвращать пару из Context и результата. Но как потом скомбинировать это действие с другим действием, которое принимает как аргументы ваш результат и Context(не как одну пару)? Ещё больше костылей. Или же вы прочитаете про монаду State и используете её.
TL DR: вы уже написали код с монадой Either, но не знаете об этом — и поэтому не пользуетесь преимуществами монад.
0
Новые инструменты есть смысл применять для решения конкретных проблем/сложностей.
Мы решали проблему декомпозиции кода, чтобы не было длинных методов (и проблем, которые с этим связаны). И метод контекстного программирования эту проблему полностью решает.
>И как раз-таки ваше «элементарное действие» это просто функция, принимающая дополнительным параметром Context и возвращающая Either Error Context.
Элементарное действие принимает контекст единственным параметром. И как правило, это не просто какая-то функция, а функция в который вызывается бизнес функция (как правило, это запрос к бизнес-сервису), результат которой сохраняется в текущий контекст. Цель элементарного действия: сделать бизнес вызов и обслужить контекст, с результатом этого вызова.
>А что если мы хотим вернуть какой-то осмысленный результат? Конечно, вы можете возвращать пару из Context и результата.
Результат нужно возвращать после применения элементарных действий над текущим контекстом. В этот момент контекст сделал свою функцию и возвращать его дальше никуда не нужно.
Поэтому про какие костыли тут речь, пока не очень понятно.
В бизнес-сервисах часто появляется State. Но, контекст — он шире State: при реализации логики State сервиса, как правило, включается в контекст, в нем обрабатывается и результирующий State в конце извлекается из контекста.
Является ли контекстное программирование заменой монадного программирование и решает ли он те же задачи, что и монады?
Конечно, не является и не решает. Я думаю, что мы даже и не ставили те же задачи, для которых были разработаны монады :).
Мы решали проблему декомпозиции кода, чтобы не было длинных методов (и проблем, которые с этим связаны). И метод контекстного программирования эту проблему полностью решает.
>И как раз-таки ваше «элементарное действие» это просто функция, принимающая дополнительным параметром Context и возвращающая Either Error Context.
Элементарное действие принимает контекст единственным параметром. И как правило, это не просто какая-то функция, а функция в который вызывается бизнес функция (как правило, это запрос к бизнес-сервису), результат которой сохраняется в текущий контекст. Цель элементарного действия: сделать бизнес вызов и обслужить контекст, с результатом этого вызова.
>А что если мы хотим вернуть какой-то осмысленный результат? Конечно, вы можете возвращать пару из Context и результата.
Результат нужно возвращать после применения элементарных действий над текущим контекстом. В этот момент контекст сделал свою функцию и возвращать его дальше никуда не нужно.
Поэтому про какие костыли тут речь, пока не очень понятно.
В бизнес-сервисах часто появляется State. Но, контекст — он шире State: при реализации логики State сервиса, как правило, включается в контекст, в нем обрабатывается и результирующий State в конце извлекается из контекста.
Является ли контекстное программирование заменой монадного программирование и решает ли он те же задачи, что и монады?
Конечно, не является и не решает. Я думаю, что мы даже и не ставили те же задачи, для которых были разработаны монады :).
-1
Может быть, решает, да. Но с моей точки зрения вы просто переизобрели велосипед — монаду Either.
> Элементарное действие принимает контекст единственным параметром.
Не вижу сложностей. Просто не делайте других параметров. И кстати, как вы тогда будете реализовывать действие «вернуть первые N записей»? Требовать, чтобы N было в вашем контексте под каким-то именем? Согласитесь, это же некрасиво. Особенно если это действие — первое в списке из нескольких. Потому что более никому ненужный параметр N так в контексте и останется.
> Результат нужно возвращать после применения элементарных действий над текущим контекстом.
Извиняюсь, не заметил, что промежуточные значения вы просто кладете в контекст. (Впрочем, until-first-error так и так возвращает updated-context, а не результат.) Только вот это порождает сразу несколько проблем. Как минимум, ломается изоляция — последующие функции видят результат всех предыдущих, если его явно не удалили из контекста. А удалять — значит это вы рассчитываете, что после функции a будет в цепочке функций всегда b, которая удалит результат a из контекста. Но тогда эти две функции разумно объединить.
>Является ли контекстное программирование заменой монадного программирование и решает ли он те же задачи, что и монады?
Не является. И не решает. Но все костыли вашего контекстного программирования заменяются использованием монады Either, даже не требуя глубокого понимания «что такое монады».
> Элементарное действие принимает контекст единственным параметром.
Не вижу сложностей. Просто не делайте других параметров. И кстати, как вы тогда будете реализовывать действие «вернуть первые N записей»? Требовать, чтобы N было в вашем контексте под каким-то именем? Согласитесь, это же некрасиво. Особенно если это действие — первое в списке из нескольких. Потому что более никому ненужный параметр N так в контексте и останется.
> Результат нужно возвращать после применения элементарных действий над текущим контекстом.
Извиняюсь, не заметил, что промежуточные значения вы просто кладете в контекст. (Впрочем, until-first-error так и так возвращает updated-context, а не результат.) Только вот это порождает сразу несколько проблем. Как минимум, ломается изоляция — последующие функции видят результат всех предыдущих, если его явно не удалили из контекста. А удалять — значит это вы рассчитываете, что после функции a будет в цепочке функций всегда b, которая удалит результат a из контекста. Но тогда эти две функции разумно объединить.
>Является ли контекстное программирование заменой монадного программирование и решает ли он те же задачи, что и монады?
Не является. И не решает. Но все костыли вашего контекстного программирования заменяются использованием монады Either, даже не требуя глубокого понимания «что такое монады».
+1
>А потом, когда вы хотите скомбинировать несколько «действий» в одно, которое бы вернуло итоговый результат или первую ошибку — вы можете написать комбинирующую функцию вручную, а можете воспользоваться тем, что Either это монада и значит её можно комбинировать с другими Either — как раз так, как вам надо. И о монадах можете продолжать не знать ничего, кроме как что они комбинируются между собой.
Ага, тут чёрт в деталях. А деталей тут как минимум две:
1) Что вернет ваша комбинирующая функция, в случае exception? И что вам делать, если внутри действий вы начинаете некоторую транзакцию?
Насколько я понимаю, этот exception просто убежит за пределы этой комбинирующей функции и в этот момент мы потеряете значение контекста, который был до exception. Чтобы вам сделать rollback в этом случае, вам как раз придется придумывать велосипеды (обертывать ваши действия в логику, которая будет сохранять контекст в кастомном exception и доставать этот контекст в catch).
В нашей реализации есть опциональная параметр-функция `on-result-fn` для `until-first-error`, в которой вы можете сделать rollback без всякого оверхеда.
2) А что вам делать, когда вы захотите сделать retry для этой операции (причем не для всех операций, а только для неуспешной)? Поскольку у вас нет контекста, а есть просто комбинирование, то вам придется делать для этого очередной велосипед.
Ага, тут чёрт в деталях. А деталей тут как минимум две:
1) Что вернет ваша комбинирующая функция, в случае exception? И что вам делать, если внутри действий вы начинаете некоторую транзакцию?
Насколько я понимаю, этот exception просто убежит за пределы этой комбинирующей функции и в этот момент мы потеряете значение контекста, который был до exception. Чтобы вам сделать rollback в этом случае, вам как раз придется придумывать велосипеды (обертывать ваши действия в логику, которая будет сохранять контекст в кастомном exception и доставать этот контекст в catch).
В нашей реализации есть опциональная параметр-функция `on-result-fn` для `until-first-error`, в которой вы можете сделать rollback без всякого оверхеда.
2) А что вам делать, когда вы захотите сделать retry для этой операции (причем не для всех операций, а только для неуспешной)? Поскольку у вас нет контекста, а есть просто комбинирование, то вам придется делать для этого очередной велосипед.
0
Только вот вы эти проблемы тоже не решаете.
1) Вы передаёте в on-result-fn контекст, каким он был до выполнения кинувшей exception функции. Ни что это за функция, ни какой был exception — вы не узнаёте. Более того, вы даже не знаете, on-result-fn вызывается после успешного выполнения или из-за ошибки, если не впихнёте это в context.
Если вам достаточно такого странного — можно сделать свой instance монады и доопределить bind. Если нет, вам тоже придется придумывать тот же самый велосипед.
2) А вам что делать? У вас нет «продолжить отсюда». Вы можете только стартовать все операции с начала, и пропускать внутри те, которые уже были выполнены — проверяя внутри это контест. Так и я могу. Или же как-то допилить ваш велосипед, дополнив его еще парой костылей.
К тому же мы сделали rollback согласно пункту 1, то операции в любом случае нужно выполнять опять.
1) Вы передаёте в on-result-fn контекст, каким он был до выполнения кинувшей exception функции. Ни что это за функция, ни какой был exception — вы не узнаёте. Более того, вы даже не знаете, on-result-fn вызывается после успешного выполнения или из-за ошибки, если не впихнёте это в context.
Если вам достаточно такого странного — можно сделать свой instance монады и доопределить bind. Если нет, вам тоже придется придумывать тот же самый велосипед.
2) А вам что делать? У вас нет «продолжить отсюда». Вы можете только стартовать все операции с начала, и пропускать внутри те, которые уже были выполнены — проверяя внутри это контест. Так и я могу. Или же как-то допилить ваш велосипед, дополнив его еще парой костылей.
К тому же мы сделали rollback согласно пункту 1, то операции в любом случае нужно выполнять опять.
0
Через контекст эти задачи можно решать достаточно гибко, в зависимости от особенностей задачи.
>Ни что это за функция, ни какой был exception — вы не узнаёте.
Цель функции on-result-fn — обслужить завершение контекста, для логики, которая не зависит от результата. Например, если в элементарных действиях делается lock на ресурс, то ее реквизиты нужно сохранить в контексте и в функции on-result-fn этот lock нужно освободить.
При этом ничто не мешает определить успешность или неуспешность выполненных действий по содержимому контента.
>Более того, вы даже не знаете, on-result-fn вызывается после успешного выполнения или из-за ошибки, если не впихнёте это в context.
Если для какой-то логики для завершения контекста результат важен, то ее нужно добавлять после получения результата (результирующий контекст или описание ошибки).
>А вам что делать? У вас нет «продолжить отсюда». Вы можете только стартовать все операции с начала, и пропускать внутри те, которые уже были выполнены — проверяя внутри это контест. Так и я могу.
А как вы сможете это сделать только через комбинирование действий, если у вас нет начального контекста?
>Ни что это за функция, ни какой был exception — вы не узнаёте.
Цель функции on-result-fn — обслужить завершение контекста, для логики, которая не зависит от результата. Например, если в элементарных действиях делается lock на ресурс, то ее реквизиты нужно сохранить в контексте и в функции on-result-fn этот lock нужно освободить.
При этом ничто не мешает определить успешность или неуспешность выполненных действий по содержимому контента.
>Более того, вы даже не знаете, on-result-fn вызывается после успешного выполнения или из-за ошибки, если не впихнёте это в context.
Если для какой-то логики для завершения контекста результат важен, то ее нужно добавлять после получения результата (результирующий контекст или описание ошибки).
>А вам что делать? У вас нет «продолжить отсюда». Вы можете только стартовать все операции с начала, и пропускать внутри те, которые уже были выполнены — проверяя внутри это контест. Так и я могу.
А как вы сможете это сделать только через комбинирование действий, если у вас нет начального контекста?
0
>Например, если в элементарных действиях делается lock на ресурс, то ее реквизиты нужно сохранить в контексте и в функции on-result-fn этот lock нужно освободить.
Допустим, вы берете лок и потом кидаете exception (в одном элементарном действии). Вопрос: откуда вы узнаете, что был взят лок? Ведь у вас есть только контекст до начала действия?
>А как мы сможете это сделать только через комбинирование действий, если у вас нет начального контекста?
Начальный контекст это то, что мы передали как параметр первому(скомбинированному) действию. Этот параметр в вызывающей функции никуда не делся, и мы можем просто использовать его еще раз.
Допустим, вы берете лок и потом кидаете exception (в одном элементарном действии). Вопрос: откуда вы узнаете, что был взят лок? Ведь у вас есть только контекст до начала действия?
>А как мы сможете это сделать только через комбинирование действий, если у вас нет начального контекста?
Начальный контекст это то, что мы передали как параметр первому(скомбинированному) действию. Этот параметр в вызывающей функции никуда не делся, и мы можем просто использовать его еще раз.
0
>Допустим, вы берете лок и потом кидаете exception (в одном элементарном действии). Вопрос: откуда вы узнаете, что был взят лок?
Элементарное действие для лока будет иметь вид:
Если exception возник в
Элементарное действие для лока будет иметь вид:
(defn get-lock [{:keys [value_a ...] :as context}]
(util/with-result-or-error
#(lock-service/create-lock value_a ... )
:lock
context))
Если exception возник в
lock-service/create-lock
, то значит лок не был создан и в context ничего сохранять не нужно, а если lock-service/create-lock
вернул успешный результат, то он сохранится в контекст (первый же тест это покажет и если он успешный, то сохранение лока в контекст всегда будет успешным).0
Сравнить контекстное программирование с программированием с монадами — действительно интересный и важный вопрос.Вы так пишете, будто «контекстное программирование» — это прямо какая-то отдельная новая парадигма :)
По сути вы написали свою реализацию монады StateT Either.
Примерно как, например, java.util.Optional — это реализация монады Maybe.
Совершенно ничего плохо в этом нету. Наоборот, это правильное дело!
Но сравнивать «контекстное программирование» с монадами неуместно.
А еще вводить новый термин там, где можно сказать «State+Either», немного излишне.
0
Допустим, что у меня есть список функций — элементарных действий и начальный контекст. Можете дать ссылку на библиотеку с реализацией монады StateT Either / State+Either / what ever, чтобы завести мою балалайку (в которую я мог бы передать этот список функций и начальный контекст)?
0
Можете дать ссылку на библиотеку с реализацией монады StateT Either / State+Either / what everДа без проблем :)
Правда у вас на самом деле просто Either, StateT даже не нужен.
Вот пример, как можно написать аналоги ваших until-first-error и with-result-on-error.
(use
'[monads.core]
'[monads.error :as e]
'[monads.types :only [either]])
(defn maybe-inc [x]
(println "call" `(maybe-inc ~x))
(if (> x 10)
(fail "Too big")
(return (inc x))))
(defn errorm->vec [m]
(either (partial vector :fail) (partial vector :ok) m))
(defn until-first-error [fs init]
(run-monad e/m (reduce >>= (return init) fs)))
(defn with-result-on-error [f key ctx]
(>>= (f) #(return (assoc ctx key %))))
(errorm->vec
(until-first-error
(repeat 5 maybe-inc)
9))
(errorm->vec
(until-first-error
[(partial with-result-on-error #(return 0) :x)
(partial with-result-on-error #(return 1) :y)
(partial with-result-on-error #(return 2) :z)]
{}))
0
А еще можно пользоваться встроенными в язык монадами either — исключениями =)
Функция until-first-error становится не нужна, вместо нее можно использовать ->, ->> или даже as->. А если список функций динамичен, тогда "((reduce comp fs) init-value)".
А функцию with-result-or-error можно реализовать вот так ;)
do-something-elementary(val) -> updated_val | (throw e)
Функция until-first-error становится не нужна, вместо нее можно использовать ->, ->> или даже as->. А если список функций динамичен, тогда "((reduce comp fs) init-value)".
А функцию with-result-or-error можно реализовать вот так ;)
(defn with-result-or-error [f k c]
(assoc c k (f))
0
Если для задачи достаточно использовать исключения, то, конечно, ни контекстный метод не нужен, ни монады :).
Для простых задач действительно достаточно
В нашем случае мы используем Optlike библиотеку, в которой exception = смерть процесса, поэтому обычный
Для простых задач действительно достаточно
->
и try/catch на верхнем уровне.В нашем случае мы используем Optlike библиотеку, в которой exception = смерть процесса, поэтому обычный
->
+ try/catch нам не подходит.+1
Спасибо за код.
Тут есть пару моментов, один — мелкий и еще один — крупный.
1) Элементарные функции в моем примере — это обычные функции. В вашем примере это — инструментированные монадами функции. Оба варианта имеют свои преимущества и недостатки.
2) Преимущество контекстного метода программирования в полную раскрывается при тестировании и при реализации retry фичи для бизнес-действия. Для этого мы используем
Для retry фичи достаточно научить элементарные действия использовать существующие данные в контексте и завернуть
Пример вызова:
В случае с монадами вам нужно еще немного инструментировать элементарные действия?
Тут есть пару моментов, один — мелкий и еще один — крупный.
1) Элементарные функции в моем примере — это обычные функции. В вашем примере это — инструментированные монадами функции. Оба варианта имеют свои преимущества и недостатки.
2) Преимущество контекстного метода программирования в полную раскрывается при тестировании и при реализации retry фичи для бизнес-действия. Для этого мы используем
until-first-error
функцию, которая возвращает последнее состояние контекста до появления ошибки:until-first-error(fs, init_context) -> [:ok updated_context] | [:error [reason last_context]]
Для retry фичи достаточно научить элементарные действия использовать существующие данные в контексте и завернуть
until-first-error
в цикл по количеству повторов:;; [:ok updated_ctx] | [:error [last_reason last_ctx]]
(defn with-retry [ctx_fn max_attempts ctx & [ on-result-fn ]]
(loop [attempt 1
last_reason nil
ctx ctx]
(if (> attempt max_attempts)
[:error [last_reason ctx]]
(match (ctx_fn ctx on-result-fn)
[:ok updated_ctx]
[:ok updated_ctx]
[:error [reason updated_ctx]]
(recur (inc attempt) reason updated_ctx)))))
Пример вызова:
(with-retry (partial util/until-first-error fs) 10 {})
В случае с монадами вам нужно еще немного инструментировать элементарные действия?
0
1. Мне не совсем понятно, что вы имеете ввиду под «инструментированные монадами». В данном случае это просто функции, возвращающие monads.types.Either. При желании это может быть defrecord или даже обычный вектор (см. мой комментарий ниже).
2. Фокус в том, что при работе с монадами (в нашем случае Either) никто не заставляет вас использовать только >>= и return. В Можно точно также написать with-retry, она даже может выглядеть 1-в-1 :)
Давайте переключимся на clojure.algo.monads.
Она стандартная и ощутимо проще. Правда не содержит реализации Either.
Но это очень легко поправить!
И это буквально все, я серьезно =)
Но m-bind не подходит для обработки ошибок, поэтому объявим его зеркального брата (при желании даже можно оформить в виде зеракальной монады either-inv-m):
Ну и пример использования:
Ну и аналог вашего with-retry выраженный через e-bind-left:
2. Фокус в том, что при работе с монадами (в нашем случае Either) никто не заставляет вас использовать только >>= и return. В Можно точно также написать with-retry, она даже может выглядеть 1-в-1 :)
Давайте переключимся на clojure.algo.monads.
Она стандартная и ощутимо проще. Правда не содержит реализации Either.
Но это очень легко поправить!
(use 'clojure.algo.monads)
(defmonad either-m
[m-bind (fn [[z v :as m] f] (if (= z :right) (f v) m))
m-result (fn [v] [:right v])])
И это буквально все, я серьезно =)
(defn maybe-inc [x]
(println "call" `(maybe-inc ~x))
(if (> x 9)
[:left "Too big"]
[:right (inc x)]))
(with-monad either-m
((m-chain ;; склеивает функции при помощи m-bind
(repeat 10 maybe-inc))
0))
Но m-bind не подходит для обработки ошибок, поэтому объявим его зеркального брата (при желании даже можно оформить в виде зеракальной монады either-inv-m):
(defn e-bind-left [[z v :as m] f] (if (= z :left) (f v) m)
Ну и пример использования:
(with-monad either-m
(-> [:right 10] ;; init
(m-bind (m-chain (repeat 5 maybe-inc)))
(e-bind-left #(do (println "Handle error" %) [:right 0]))
(m-bind (m-chain (repeat 5 maybe-inc)))))
Ну и аналог вашего with-retry выраженный через e-bind-left:
(defn e-with-retry [f]
(if (<= attempts 0)
(fn [v] (e-bind-left (f v)
(fn [e] [:left [e v]])))
(fn [v] (e-bind-left (f v)
(fn [_] ((e-with-retry f (dec n)) v))))))
+1
Упс, последнюю функцию неправильно скопировал. Должно быть
Вообще монада Either в данном случае — это по сути паттерн.
Как всем известны паттерны для ООП (адаптер, мост, декоратор и т.п.), так и для ФП можно вывести шаблонные подходы. Достаточно сказать новому члену команды «а вот тут у нас своя реализация Either с блекджеком и шлюхами». И все, не нужно объяснять что такое «контекст», путатьтся с другими «контекстами» (уж очень популярный термин), подробно расписывать документацию и т.п.
(defn e-with-retry [f n]
(if (<= n 0)
(fn [v] (e-bind-left (f v)
(fn [e] [:left [e v]])))
(fn [v] (e-bind-left (f v)
(fn [_] ((e-with-retry f (dec n)) v))))))
Вообще монада Either в данном случае — это по сути паттерн.
Как всем известны паттерны для ООП (адаптер, мост, декоратор и т.п.), так и для ФП можно вывести шаблонные подходы. Достаточно сказать новому члену команды «а вот тут у нас своя реализация Either с блекджеком и шлюхами». И все, не нужно объяснять что такое «контекст», путатьтся с другими «контекстами» (уж очень популярный термин), подробно расписывать документацию и т.п.
0
Этих двух сущностей: элементарное действие и контекст, достаточно, чтобы эффективно реализовать бОльшую часть бизнес логики.
Те, кому нравятся монады — используют монады.
Те, кому не нравятся монады, могут использовать эту простую методологию.
Те, кому нравятся монады — используют монады.
Те, кому не нравятся монады, могут использовать эту простую методологию.
0
until-first-error — какой кошмар, думаю одной этой функцией можно было бы закончить статью, сейчас копаюсь в подобном коде, это какой-то ужас, люди, никогда не делайте так, чтобы другим не было мучительно больно, функции с бизнес-логикой должны быть названы с точки зрения бизнес-логики и использовать термины бизнес-логики, не надо абстракции абстракциями погонять, это дорога в мир ужасов разработчика.
0
Не надо перекладывать с больной головы на здоровую ;).
Контекстный подход успешно решает:
1) проблему декомпозиции кода (когда код метода не влезает в один экран);
2) упрощает тестирование и, как результат, ускоряет разработку;
3) предлагает альтернативный вариант к гибкой реализации retry фичи, когда можно делать retry не всей операции полностью, а только тех шагов, которые нужны.
А, и да, самое главное:
Disclaimer: этот метод не улучшает качество кода криворукого разработчика, а наоборот ухудшает его экспоненциально :).
Контекстный подход успешно решает:
1) проблему декомпозиции кода (когда код метода не влезает в один экран);
2) упрощает тестирование и, как результат, ускоряет разработку;
3) предлагает альтернативный вариант к гибкой реализации retry фичи, когда можно делать retry не всей операции полностью, а только тех шагов, которые нужны.
А, и да, самое главное:
Disclaimer: этот метод не улучшает качество кода криворукого разработчика, а наоборот ухудшает его экспоненциально :).
0
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
О декомпозии кода замолвим слово: контекстное программирование