From b9521f1aeabd1ef4607e7d5f1edb6cad9063f6a4 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sun, 7 Jun 2020 20:30:04 +0400 Subject: [PATCH 01/16] save --- Readme.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/Readme.md b/Readme.md index 54cbf60..500ad06 100644 --- a/Readme.md +++ b/Readme.md @@ -3,6 +3,125 @@ # Effect +Алгебраические эффекты для Clojure(Script). + +## Rationale + +Я здумывался о том, как отделить логику приложения от деталей реализации. + +Под логикой я понимаю код, описывающий принятие решений. +Это ветвление, циклы, обработка исключений. Этот код наиболее далек по стеку вызыов от ввода/вывода. +Этот код написан в функциональной парадигме. + +Под деталями реализации я понимаю код, взаимодействующий с внешним миром, наиболее близкий к вводу/выводу. +Этот код максимально прямолинейный, написанный в императивной парадигме. + +Имея такое разделение, становится возможным начинать разработку с высокоуровневой логики, +дешево проверять гипотезы, находить противоречия в функциональных требованиях. + +Отмечу, что это разделение происходит только на уровне кода. +Вы по прежнему должны задумываться о деталях. +Но только задумываться, а не реализовывать их в начале работы, когда требования постоянно меняются. +Сможет ли какая-либо база данных исполнить задуманный запрос? +Сможет ли сторонний сервис выдержать в будущем создаваемую нами нагрузку? +От куда мы будем получать все необходимые данные? + +Эта идея перекликается с +* functional core imperative shell +* clean architecture +* ports and adapters + +Однако теория разбивается о реальность. + +Предположим, у нас есть функция, описывающая логин пользователя: + +```clojure +(declare ^:dynamic *get-session* + ^:dynamic *update-session* + ^:dynamic *get-user-by-login* + ^:dynamic *check-password*) + +(defn login [{:as form :keys [login password]}] + (let [session (*get-session*)] + (if (contains? session :user-id) + {:type :already-logged-in} + (let [{:as user :keys [id password-digest]} (*get-user-by-login* login)] + (if (or (nil? user) + (*check-password* password password-digest)) + {:type :wrong-login-or-password} + (do + (*update-session* assoc :user-id id) + {:type :processed})))))) +``` + +Функции вроде `*get-user-by-login*` - зависимости, детали реализации. +С помощью `bindings` можно установить заглушку и протестировать логику +до появления промышленной реализации зависимостей. + +Внедрение зависимостей можно реализовать и через +статические переменные и `with-redefs`, замыкания или передачу контекста: + +```clojure +(defn ->login-2 [get-session update-session get-user-by-login check-password] + (fn login [form])) + +(defn login-3 [ctx form]) +``` + +Можно ошибочно предположить, что функция `login` чистая, ведь она не вызывает побочные эффекты. +Однако зависимости, по своей природе, взаимодействуют с вводом/выводом. +Вы же хотите получать данные пользователя из базы данных? + +Давайте для наглядности рассмотрим сигнатуру функции `->login` в haskell нотации + +```haskell +build_login_2 :: IO SessionData -> + ((SessionData -> SessionData) -> IO SessionData) + (String -> IO UserData) + (String -> String -> IO Boolean) + FormData -> ResponseData +``` + +Получается, что функция `login` не чистая, т.к. вызывает не чистые фукнции. +Но изначально я хотел, чтобы ядро приложения было чистым. +Как сделать так, чтобы императивная оболочка вызывала чистое ядро? + +Что, если не принимать зависимости, а возвращать описание побочного эффекта и продолжение функции? + +```clojure +(defn login-4 [{:as form :keys [login password]}] + [[:get-session] + (fn [session] + ;;... + [[:get-user-by-login login] + (fn [user] + ;; ... + [[:check-password password password-digest] + (fn [correct-password?] + ;; ... + [[:update-session assoc :user-id id] + (fn [session] + ;; ... + )])])])]) +``` + +Т.е. `login-4` возвращает `[effect-description continuation-1]`, +первое продолжение в свою очередь возвращает эффект и второе продолжение. + +Теперь нам нужен императивный интерпретатор эффектов вызывающий нашу чистую функцию и ее чистые продолжения. + + + + + + + + + +https://blog.ploeh.dk/2017/02/02/dependency-rejection/ + + + ## Development ``` From cbd5632ec8ebd090cd3c52d44abdc2fc6ec0b2a0 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sun, 7 Jun 2020 20:40:53 +0400 Subject: [PATCH 02/16] save --- Readme.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index 500ad06..bbd6ffa 100644 --- a/Readme.md +++ b/Readme.md @@ -84,8 +84,12 @@ build_login_2 :: IO SessionData -> Получается, что функция `login` не чистая, т.к. вызывает не чистые фукнции. Но изначально я хотел, чтобы ядро приложения было чистым. -Как сделать так, чтобы императивная оболочка вызывала чистое ядро? +Подробнее об этой проблеме вы можете прочитать в статье +[Dependency rejection](https://blog.ploeh.dk/2017/02/02/dependency-rejection/). + +Как сделать так, чтобы императивная оболочка вызывала чистое ядро? +И не разбивать единое вычисление на множество отедельных не связанных между собой чистых шагов? Что, если не принимать зависимости, а возвращать описание побочного эффекта и продолжение функции? ```clojure @@ -108,7 +112,9 @@ build_login_2 :: IO SessionData -> Т.е. `login-4` возвращает `[effect-description continuation-1]`, первое продолжение в свою очередь возвращает эффект и второе продолжение. -Теперь нам нужен императивный интерпретатор эффектов вызывающий нашу чистую функцию и ее чистые продолжения. +Теперь мы можем написать императивный интерпретатор эффектов +вызывающий нашу чистую функцию и ее чистые продолжения. + @@ -118,7 +124,6 @@ build_login_2 :: IO SessionData -> -https://blog.ploeh.dk/2017/02/02/dependency-rejection/ From 6fffef5ea09a94d7a0eff3dfe8c164b2ba59c0aa Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sun, 7 Jun 2020 21:11:20 +0400 Subject: [PATCH 03/16] readme --- .circleci/config.yml | 3 ++- Readme.md => README.md | 3 ++- deps.edn | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) rename Readme.md => README.md (99%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 10110c0..f9d26db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,7 +18,7 @@ jobs: keys: - v1-dependencies-{{ checksum "project.clj" }}-{{ checksum "deps.edn" }} - - run: clojure -R:dev:clj-test:cljs-test -e "(prn :deps)" + - run: clojure -R:dev:clj-test:cljs-test:readme -e "(prn :deps)" - save_cache: paths: @@ -28,3 +28,4 @@ jobs: - run: clojure -A:dev:clj-test - run: clojure -A:dev:cljs-test + - run: clojure -A:readme diff --git a/Readme.md b/README.md similarity index 99% rename from Readme.md rename to README.md index bbd6ffa..e52e568 100644 --- a/Readme.md +++ b/README.md @@ -33,6 +33,7 @@ Однако теория разбивается о реальность. + Предположим, у нас есть функция, описывающая логин пользователя: ```clojure @@ -98,7 +99,7 @@ build_login_2 :: IO SessionData -> (fn [session] ;;... [[:get-user-by-login login] - (fn [user] + (fn [{:as user :keys [id password-digest]}] ;; ... [[:check-password password password-digest] (fn [correct-password?] diff --git a/deps.edn b/deps.edn index 8d61169..4ca5084 100644 --- a/deps.edn +++ b/deps.edn @@ -5,4 +5,6 @@ :sha "f7ef16dc3b8332b0d77bc0274578ad5270fbfedd"}} :main-opts ["-m" "cognitect.test-runner"]} :cljs-test {:extra-deps {olical/cljs-test-runner {:mvn/version "3.7.0"}} - :main-opts ["-m" "cljs-test-runner.main"]}}} + :main-opts ["-m" "cljs-test-runner.main"]} + :readme {:extra-deps {seancorfield/readme {:mvn/version "1.0.13"}} + :main-opts ["-m" "seancorfield.readme"]}}} From b46f15208af2e7377d1a5add4a2af223a151c4a7 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sun, 7 Jun 2020 22:20:23 +0400 Subject: [PATCH 04/16] save --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e52e568..9646d5f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ [![Clojars Project](https://img.shields.io/clojars/v/darkleaf/effect.svg)](https://clojars.org/darkleaf/effect) [![CircleCI](https://circleci.com/gh/darkleaf/effect.svg?style=svg)](https://circleci.com/gh/darkleaf/effect) -# Effect - Алгебраические эффекты для Clojure(Script). -## Rationale +# Rationale Я здумывался о том, как отделить логику приложения от деталей реализации. @@ -48,7 +46,7 @@ {:type :already-logged-in} (let [{:as user :keys [id password-digest]} (*get-user-by-login* login)] (if (or (nil? user) - (*check-password* password password-digest)) + (not (*check-password* password password-digest))) {:type :wrong-login-or-password} (do (*update-session* assoc :user-id id) @@ -116,22 +114,83 @@ build_login_2 :: IO SessionData -> Теперь мы можем написать императивный интерпретатор эффектов вызывающий нашу чистую функцию и ее чистые продолжения. +# Effect +Если вы не знакомы с концепцией эффектов, то прочитайте +[Algebraic Effects for the Rest of Us](https://overreacted.io/algebraic-effects-for-the-rest-of-us/) +Функция `login-4` выглядит устрашающе и порождает "callback hell'. +К счастью у нас есть макросы и мы можем скрыть эту деталь от пользователя. +```clojure +(require '[darkleaf.effect.core :as e :refer [effect with-effects !]]) +``` +Макрос `with-effects` делает всю работу. В местах, помеченных `!` происходит разрыв фукнции. +```clojure +(defn login-5 [{:as form :keys [login password]}] + (with-effects + (let [session (! (effect :get-session))] + (if (contains? session :user-id) + {:type :already-logged-in} + (let [{:as user :keys [id password-digest]} (! (effect :get-user-by-login login))] + (if (or (nil? user) + (not (! (effect :check-password password password-digest)))) + {:type :wrong-login-or-password} + (do + (! (effect :update-session assoc :user-id id)) + {:type :processed}))))))) +``` +```clojure +(let [cont (e/continuation login-5) + [effect cont-1] (cont [{:login "joe" :password "secret"}])] + [effect (fn? cont-1)]) +=> [[:get-session] true] +``` +Давайте определим обработчики эффектов в виде заглушек +```clojure +(let [handlers {:get-session (fn [] {}) + :update-session (fn [& _] :unused) + :get-user-by-login (fn [login] {:id 1 + :password-digest "digest"}) + :check-password (fn [password digest] true)} + cont (e/continuation login-5)] + (e/perform handlers cont [{:login "joe" :password "secret"}])) +=> {:type :processed} +``` +Двайте напишем тест на нашу функцию с использованием сценария эффектов: +```clojure +(require '[clojure.test :as t]) +(require '[darkleaf.effect.script :as script]) +``` + +```clojure +(t/deftest login-5-test + (let [cont (e/continuation login-5) + script [{:args [{:login "joe" :password "secret"}]} + {:effect [:get-session] + :coeffect {}} + {:effect [:get-user-by-login "joe"] + :coeffect {:id 1 :password-digest "digest"}} + {:effect [:check-password "secret" "digest"] + :coeffect true} + {:effect [:update-session assoc :user-id 1] + :coeffect :unused} + {:return {:type :processed}}]] + (script/test cont script))) +``` -## Development -``` -lein test -lein doo node node-none once -lein doo node node-advanced once -``` + + + + + +про ассинхронный ввод вывод рассказать в `perform` From 9dbbb0faced7ad156648ee5f7bbc92a12f81a0ef Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sun, 7 Jun 2020 23:36:30 +0400 Subject: [PATCH 05/16] save --- README.md | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9646d5f..22adf31 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,18 @@ Алгебраические эффекты для Clojure(Script). +# Api + +* [core test](test/darkleaf/effect/core_test.cljc). +* [script test](test/darkleaf/effect/script_test.cljc). +* [core analogs test](test/darkleaf/effect/core_analogs_test.cljc). +* middleware + * [composition test](test/darkleaf/effect/middleware/composition_test.cljc). + * [context test](test/darkleaf/effect/middleware/context_test.cljc). + * [contract test](test/darkleaf/effect/middleware/contract_test.cljc). + * [log test](test/darkleaf/effect/middleware/log_test.cljc). + * [reduced test](test/darkleaf/effect/middleware/reduced_test.cljc). + # Rationale Я здумывался о том, как отделить логику приложения от деталей реализации. @@ -127,6 +139,8 @@ build_login_2 :: IO SessionData -> ``` Макрос `with-effects` делает всю работу. В местах, помеченных `!` происходит разрыв фукнции. +Фукнция `effect` показывает, что мы прерываемся на вызов эффекта, а не другой фукнции. +Можно провести некоторую аналогию между `with-effects/!` и `async/await` или `core.async`. ```clojure (defn login-5 [{:as form :keys [login password]}] @@ -150,7 +164,7 @@ build_login_2 :: IO SessionData -> => [[:get-session] true] ``` -Давайте определим обработчики эффектов в виде заглушек +Давайте определим обработчики эффектов и запустим нашу фукнцию с помощью `e/perform`. ```clojure (let [handlers {:get-session (fn [] {}) @@ -163,7 +177,9 @@ build_login_2 :: IO SessionData -> => {:type :processed} ``` -Двайте напишем тест на нашу функцию с использованием сценария эффектов: +## Script testing + +Такой подход плохо подходит для тестирования, поэтому давайте протестируем функцию с использованием сценария: ```clojure (require '[clojure.test :as t]) @@ -186,11 +202,26 @@ build_login_2 :: IO SessionData -> (script/test cont script))) ``` +Сценарий проверят какие и в каком порядке были запрошены эффекты +и какие коэффекты нужно передать в обратно программу. +Expected effect сравнивается с actual effect по значению с помощью `clojure.core/=`. +Также скрипт может проверять брошенные исключения с помощью `:thrown` +или обрывать проверку на заданном эффекте с помощью `:final-effect`. +По аналогии с async/await поддержка эффектов делит функции на "цвета". +Подробности вы найдете в статье [What Color is Your Function?](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). +Т.е. обычная функция не может вызвать фукнцию с эффектами. +Например, вы не можете передавать функции с эффектами в `clojure.core/map`. +Есть надежда на то, что для JVM эту проблему решит [Project Loom](https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html). +Но пока вы можете воспользоваться фукнциями и макросами из `darkleaf.effect.core-analogs`. +## Middlewares +## Async handlers +## Internals +https://github.com/leonoel/cloroutine -про ассинхронный ввод вывод рассказать в `perform` +core.async/go From 4c2e26035fcafc827deb7736f97d1546242873f7 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 10 Jun 2020 00:18:00 +0400 Subject: [PATCH 06/16] save --- README.md | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 162 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 22adf31..0fd543f 100644 --- a/README.md +++ b/README.md @@ -206,22 +206,180 @@ build_login_2 :: IO SessionData -> и какие коэффекты нужно передать в обратно программу. Expected effect сравнивается с actual effect по значению с помощью `clojure.core/=`. Также скрипт может проверять брошенные исключения с помощью `:thrown` -или обрывать проверку на заданном эффекте с помощью `:final-effect`. +или обрывать проверку на заданном эффекте с помощью `:final-effect` +```clojure +{:thrown {:type RuntimeException + :mssage "Some message" + :data nil}} +{:final-effect [:early-return :some-value]} +``` + +## Stack + +Функция с эффектами может вызывать другую функцию с эффектами или без + +```clojure +(t/deftest stack-use-case + (let [nested-ef (fn [x] + (with-effects + (! (effect :prn :nested "start")) + (! (effect :prn :nested x)) + (! (str "nested: " x)))) + ef (fn [x] + (with-effects + (! (effect :prn :main "start")) + (! (nested-ef x)))) + continuation (e/continuation ef) + script [{:args ["arg"]} + {:effect [:prn :main "start"] + :coeffect nil} + {:effect [:prn :nested "start"] + :coeffect nil} + {:effect [:prn :nested "arg"] + :coeffect nil} + {:return "nested: arg"}]] + (script/test continuation script))) +``` + +`(! (nested-ef x))` - вызов функции с эффектами. +Если `nested-ef` перестанет использовать макрос `with-effects`, +то `!` просто вернет вычисленное значение. + +Вы можете использовать `!` с эффектами, функциями с эффектами, значениями и обычными фукнциями. + +## Core analogs По аналогии с async/await поддержка эффектов делит функции на "цвета". Подробности вы найдете в статье [What Color is Your Function?](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). Т.е. обычная функция не может вызвать фукнцию с эффектами. Например, вы не можете передавать функции с эффектами в `clojure.core/map`. Есть надежда на то, что для JVM эту проблему решит [Project Loom](https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html). -Но пока вы можете воспользоваться фукнциями и макросами из `darkleaf.effect.core-analogs`. +Но пока вы можете воспользоваться фукнциями и макросами из +[`darkleaf.effect.core-analogs`](test/darkleaf/effect/core_analogs_test.cljc). ## Middlewares +Продолжение возвращает пару из эффекта и следующего продолжения. +Если вычисление завершено, то возвращается пара из результата и `nil`. +Таким образом, мы можем управлять вычислением. + +Рассмотрим пустую middlware: + +```clojure +(defn wrap-blank [continuation] + (when (some? continuation) + (fn [coeffect] + (let [[effect continuation] (continuation coeffect)] + [effect (wrap-blank continuation)])))) + +(let [continuation (-> login-5 + (e/continuation) + (wrap-blank))] + #_"some code") +``` + +С помощью [context middleware](test/darkleaf/effect/middleware/context_test.cljc) +вы можете передавать контест между обработчиками эффектов. +Это напоминает монады State, Reader и Writer. + +```clojure +(require '[darkleaf.effect.middleware.context :as context]) +``` + +```clojure +(let [ef (fn [arg] + (with-effects + (! (effect :ctx/update inc)) + (! (effect :ctx/update + 2)) + arg)) + continuation (-> ef + (e/continuation) + (context/wrap-context)) + handlers {:ctx/update (fn [context f & args] + (let [new-context (apply f context args)] + [new-context new-context]))}] + (e/perform handlers continuation [0 [:some-arg]])) +=> [3 :some-arg] +``` + +После применения `context/wrap-context` обработчики принимают контекст первым дополнительным аргументом +и должны возвращать пару из контекта и коэффекта. + +С помощью [reduced middleware](test/darkleaf/effect/middleware/reduced_test.cljc) +вы можете досрочно прервать вычисление. Это напоминает монады Maybe или Either. + +```clojure +(require '[darkleaf.effect.middleware.reduced :as reduced]) +``` + +```clojure +(let [ef (fn [x] + (with-effects + (+ 5 (! (effect :maybe x))))) + handlers {:maybe (fn [value] + (if (nil? value) + (reduced nil) + value))} + continuation (-> ef + (e/continuation) + (reduced/wrap-reduced))] + [(e/perform handlers continuation [1]) + (e/perform handlers continuation [nil])]) +=> [6 nil] +``` + +Если обработчик возвращает `reduced` значение, то вычисление прерывается и это значение +используется для возврата из функции. + +С помощью [contract middleware](test/darkleaf/effect/middleware/contract_test.cljc) +вы можете проверять контракты фукнций и их эффектов/коэффектов. + +```clojure +(require '[darkleaf.effect.middleware.contract :as contract]) +``` + +```clojure +(let [effn (fn [x] + (with-effects + (+ x (! (effect :my/effect 1))))) + contract {'my/effn {:args (fn [x] (int? x)) + :return int?} + :my/effect {:effect (fn [x] (int? x)) + :coeffect int?}} + continuation (-> effn + (e/continuation) + (contract/wrap-contract contract 'my/effn))]) +``` + +С помощью [log middleware](test/darkleaf/effect/middleware/log_test.cljc) +вы можете вести журнал эффектов, что позволяет замораживать и продолжать вычисление. +Журнал может быть сериализован, передан на другую машину и применен для продолжения вычисления. +Вы можете начать вычисление на сервере и продолжить его в браузере и наоброт. + +Middlware можно объединять. Подробнее в [composition test](test/darkleaf/effect/middleware/composition_test.cljc). + ## Async handlers +Вы можете писать код с эффектами, синхронно его тестировать, но запускать с ассинхронными обработчиками. +Для этого случая функция `e/perform` принимает доплнительные агрументы `respond` и `raise`. + +```clojure +(comment + (defn perform + ([handlers continuation coeffect-or-args]) + ([handlers continuation coeffect-or-args respond raise]))) +``` + +Ассинхронный обработчик так же принимать 2 дополнительных агрумента: `respond` и `raise` + +```clojure +(comment + (defn my-effect-handler + ([arg-1 arg-2]) + ([arg-1 arg-2 respond raise]))) +``` + ## Internals https://github.com/leonoel/cloroutine - -core.async/go From 11451d22c6fb978dafc84a980d0fe367182aaff9 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 10 Jun 2020 00:22:12 +0400 Subject: [PATCH 07/16] typos --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0fd543f..838d3f7 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ # Rationale -Я здумывался о том, как отделить логику приложения от деталей реализации. +Я задумывался о том, как отделить логику приложения от деталей реализации. Под логикой я понимаю код, описывающий принятие решений. -Это ветвление, циклы, обработка исключений. Этот код наиболее далек по стеку вызыов от ввода/вывода. +Это ветвление, циклы, обработка исключений. Этот код наиболее далек по стеку вызовов от ввода/вывода. Этот код написан в функциональной парадигме. Под деталями реализации я понимаю код, взаимодействующий с внешним миром, наиболее близкий к вводу/выводу. @@ -93,14 +93,14 @@ build_login_2 :: IO SessionData -> FormData -> ResponseData ``` -Получается, что функция `login` не чистая, т.к. вызывает не чистые фукнции. +Получается, что функция `login` не чистая, т.к. вызывает не чистые функции. Но изначально я хотел, чтобы ядро приложения было чистым. Подробнее об этой проблеме вы можете прочитать в статье [Dependency rejection](https://blog.ploeh.dk/2017/02/02/dependency-rejection/). Как сделать так, чтобы императивная оболочка вызывала чистое ядро? -И не разбивать единое вычисление на множество отедельных не связанных между собой чистых шагов? +И не разбивать единое вычисление на множество отдельных не связанных между собой чистых шагов? Что, если не принимать зависимости, а возвращать описание побочного эффекта и продолжение функции? ```clojure @@ -138,8 +138,8 @@ build_login_2 :: IO SessionData -> (require '[darkleaf.effect.core :as e :refer [effect with-effects !]]) ``` -Макрос `with-effects` делает всю работу. В местах, помеченных `!` происходит разрыв фукнции. -Фукнция `effect` показывает, что мы прерываемся на вызов эффекта, а не другой фукнции. +Макрос `with-effects` делает всю работу. В местах, помеченных `!` происходит разрыв функции. +Функция `effect` показывает, что мы прерываемся на вызов эффекта, а не другой функции. Можно провести некоторую аналогию между `with-effects/!` и `async/await` или `core.async`. ```clojure @@ -164,7 +164,7 @@ build_login_2 :: IO SessionData -> => [[:get-session] true] ``` -Давайте определим обработчики эффектов и запустим нашу фукнцию с помощью `e/perform`. +Давайте определим обработчики эффектов и запустим нашу функцию с помощью `e/perform`. ```clojure (let [handlers {:get-session (fn [] {}) @@ -209,9 +209,9 @@ Expected effect сравнивается с actual effect по значению или обрывать проверку на заданном эффекте с помощью `:final-effect` ```clojure -{:thrown {:type RuntimeException - :mssage "Some message" - :data nil}} +{:thrown {:type RuntimeException + :message "Some message" + :data nil}} {:final-effect [:early-return :some-value]} ``` @@ -246,16 +246,16 @@ Expected effect сравнивается с actual effect по значению Если `nested-ef` перестанет использовать макрос `with-effects`, то `!` просто вернет вычисленное значение. -Вы можете использовать `!` с эффектами, функциями с эффектами, значениями и обычными фукнциями. +Вы можете использовать `!` с эффектами, функциями с эффектами, значениями и обычными функциями. ## Core analogs По аналогии с async/await поддержка эффектов делит функции на "цвета". Подробности вы найдете в статье [What Color is Your Function?](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). -Т.е. обычная функция не может вызвать фукнцию с эффектами. +Т.е. обычная функция не может вызвать функцию с эффектами. Например, вы не можете передавать функции с эффектами в `clojure.core/map`. Есть надежда на то, что для JVM эту проблему решит [Project Loom](https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html). -Но пока вы можете воспользоваться фукнциями и макросами из +Но пока вы можете воспользоваться функциями и макросами из [`darkleaf.effect.core-analogs`](test/darkleaf/effect/core_analogs_test.cljc). ## Middlewares @@ -264,7 +264,7 @@ Expected effect сравнивается с actual effect по значению Если вычисление завершено, то возвращается пара из результата и `nil`. Таким образом, мы можем управлять вычислением. -Рассмотрим пустую middlware: +Рассмотрим пустую middleware: ```clojure (defn wrap-blank [continuation] @@ -280,7 +280,7 @@ Expected effect сравнивается с actual effect по значению ``` С помощью [context middleware](test/darkleaf/effect/middleware/context_test.cljc) -вы можете передавать контест между обработчиками эффектов. +вы можете передавать контекст между обработчиками эффектов. Это напоминает монады State, Reader и Writer. ```clojure @@ -304,7 +304,7 @@ Expected effect сравнивается с actual effect по значению ``` После применения `context/wrap-context` обработчики принимают контекст первым дополнительным аргументом -и должны возвращать пару из контекта и коэффекта. +и должны возвращать пару из контекста и коэффекта. С помощью [reduced middleware](test/darkleaf/effect/middleware/reduced_test.cljc) вы можете досрочно прервать вычисление. Это напоминает монады Maybe или Either. @@ -333,7 +333,7 @@ Expected effect сравнивается с actual effect по значению используется для возврата из функции. С помощью [contract middleware](test/darkleaf/effect/middleware/contract_test.cljc) -вы можете проверять контракты фукнций и их эффектов/коэффектов. +вы можете проверять контракты функций и их эффектов/коэффектов. ```clojure (require '[darkleaf.effect.middleware.contract :as contract]) @@ -355,14 +355,14 @@ Expected effect сравнивается с actual effect по значению С помощью [log middleware](test/darkleaf/effect/middleware/log_test.cljc) вы можете вести журнал эффектов, что позволяет замораживать и продолжать вычисление. Журнал может быть сериализован, передан на другую машину и применен для продолжения вычисления. -Вы можете начать вычисление на сервере и продолжить его в браузере и наоброт. +Вы можете начать вычисление на сервере и продолжить его в браузере и наоборот. -Middlware можно объединять. Подробнее в [composition test](test/darkleaf/effect/middleware/composition_test.cljc). +Middleware можно объединять. Подробнее в [composition test](test/darkleaf/effect/middleware/composition_test.cljc). ## Async handlers -Вы можете писать код с эффектами, синхронно его тестировать, но запускать с ассинхронными обработчиками. -Для этого случая функция `e/perform` принимает доплнительные агрументы `respond` и `raise`. +Вы можете писать код с эффектами, синхронно его тестировать, но запускать с асинхронными обработчиками. +Для этого случая функция `e/perform` принимает дополнительные аргументы `respond` и `raise`. ```clojure (comment @@ -371,7 +371,7 @@ Middlware можно объединять. Подробнее в [composition te ([handlers continuation coeffect-or-args respond raise]))) ``` -Ассинхронный обработчик так же принимать 2 дополнительных агрумента: `respond` и `raise` +Асинхронный обработчик так же принимать 2 дополнительных аргумента: `respond` и `raise` ```clojure (comment From f8876e60de2a559abdedab0668a491fc51b4a29f Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 10 Jun 2020 00:33:24 +0400 Subject: [PATCH 08/16] save --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 838d3f7..4688cdc 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,14 @@ От куда мы будем получать все необходимые данные? Эта идея перекликается с -* functional core imperative shell +* functional core, imperative shell * clean architecture * ports and adapters Однако теория разбивается о реальность. -Предположим, у нас есть функция, описывающая логин пользователя: +Предположим, у нас есть функция, описывающая процесс входа пользователя в систему: ```clojure (declare ^:dynamic *get-session* @@ -203,7 +203,7 @@ build_login_2 :: IO SessionData -> ``` Сценарий проверят какие и в каком порядке были запрошены эффекты -и какие коэффекты нужно передать в обратно программу. +и какие коэффекты нужно передать обратно в программу. Expected effect сравнивается с actual effect по значению с помощью `clojure.core/=`. Также скрипт может проверять брошенные исключения с помощью `:thrown` или обрывать проверку на заданном эффекте с помощью `:final-effect` @@ -244,7 +244,7 @@ Expected effect сравнивается с actual effect по значению `(! (nested-ef x))` - вызов функции с эффектами. Если `nested-ef` перестанет использовать макрос `with-effects`, -то `!` просто вернет вычисленное значение. +то `!` просто вернет вычисленное значение. `(! (str "nested: " x))` как раз демонстрирует это поведение. Вы можете использовать `!` с эффектами, функциями с эффектами, значениями и обычными функциями. @@ -255,7 +255,7 @@ Expected effect сравнивается с actual effect по значению Т.е. обычная функция не может вызвать функцию с эффектами. Например, вы не можете передавать функции с эффектами в `clojure.core/map`. Есть надежда на то, что для JVM эту проблему решит [Project Loom](https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html). -Но пока вы можете воспользоваться функциями и макросами из +А пока вы можете воспользоваться функциями и макросами из [`darkleaf.effect.core-analogs`](test/darkleaf/effect/core_analogs_test.cljc). ## Middlewares @@ -371,13 +371,14 @@ Middleware можно объединять. Подробнее в [composition t ([handlers continuation coeffect-or-args respond raise]))) ``` -Асинхронный обработчик так же принимать 2 дополнительных аргумента: `respond` и `raise` +Асинхронный обработчик так же должнен принимать 2 дополнительных аргумента для ассинхронного случая ```clojure (comment - (defn my-effect-handler - ([arg-1 arg-2]) - ([arg-1 arg-2 respond raise]))) + (defn my-identity-handler + ([x] x) + ([x respond raise] + (js/process.nextTick respond x)))) ``` ## Internals From 2f90a5c6775be32edc250e4c619885e4f0c11c31 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Fri, 12 Jun 2020 23:09:54 +0400 Subject: [PATCH 09/16] save --- README.md | 135 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 4688cdc..602a293 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,10 @@ От куда мы будем получать все необходимые данные? Эта идея перекликается с -* functional core, imperative shell -* clean architecture -* ports and adapters - -Однако теория разбивается о реальность. - +* Functional core, imperative shell +* [Clean architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +* [Hexagonal architecture](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) +* [Ports and adapters](http://www.dossier-andreas.net/software_architecture/ports_and_adapters.html) Предположим, у нас есть функция, описывающая процесс входа пользователя в систему: @@ -99,9 +97,15 @@ build_login_2 :: IO SessionData -> Подробнее об этой проблеме вы можете прочитать в статье [Dependency rejection](https://blog.ploeh.dk/2017/02/02/dependency-rejection/). -Как сделать так, чтобы императивная оболочка вызывала чистое ядро? +Стоит также рассмотреть зависимости между функциями. Есть 2 типа зависимостей: +compile time и run time. Суть внедрения зависимостей в инверсии compile time зависимостей, +т.е. в compile time оболочка зависит (реализует неявный интерфейс) от ядра, +но в run time ядро зависит (вызывает) от оболочки. + +А как сделать так, чтобы императивная оболочка вызывала чистое ядро? И не разбивать единое вычисление на множество отдельных не связанных между собой чистых шагов? -Что, если не принимать зависимости, а возвращать описание побочного эффекта и продолжение функции? +Что, если не принимать зависимости, а возвращать описание побочного эффекта и +продолжение функции, принимающее результат исполнения этого эффекта? ```clojure (defn login-4 [{:as form :keys [login password]}] @@ -139,7 +143,7 @@ build_login_2 :: IO SessionData -> ``` Макрос `with-effects` делает всю работу. В местах, помеченных `!` происходит разрыв функции. -Функция `effect` показывает, что мы прерываемся на вызов эффекта, а не другой функции. +Функция `effect` показывает, что мы прерываемся на вызов эффекта. Можно провести некоторую аналогию между `with-effects/!` и `async/await` или `core.async`. ```clojure @@ -157,6 +161,8 @@ build_login_2 :: IO SessionData -> {:type :processed}))))))) ``` +`e/continuation` преобразует функцию с эффектами в продолжение: + ```clojure (let [cont (e/continuation login-5) [effect cont-1] (cont [{:login "joe" :password "secret"}])] @@ -179,7 +185,7 @@ build_login_2 :: IO SessionData -> ## Script testing -Такой подход плохо подходит для тестирования, поэтому давайте протестируем функцию с использованием сценария: +Такой подход плохо подходит для тестирования, поэтому давайте протестируем функцию используя сценарий: ```clojure (require '[clojure.test :as t]) @@ -203,7 +209,7 @@ build_login_2 :: IO SessionData -> ``` Сценарий проверят какие и в каком порядке были запрошены эффекты -и какие коэффекты нужно передать обратно в программу. +и какие коэффекты нужно передать обратно. Expected effect сравнивается с actual effect по значению с помощью `clojure.core/=`. Также скрипт может проверять брошенные исключения с помощью `:thrown` или обрывать проверку на заданном эффекте с помощью `:final-effect` @@ -217,7 +223,7 @@ Expected effect сравнивается с actual effect по значению ## Stack -Функция с эффектами может вызывать другую функцию с эффектами или без +Функция с эффектами может вызывать другие функции с эффектами или без ```clojure (t/deftest stack-use-case @@ -250,14 +256,40 @@ Expected effect сравнивается с actual effect по значению ## Core analogs -По аналогии с async/await поддержка эффектов делит функции на "цвета". -Подробности вы найдете в статье [What Color is Your Function?](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). -Т.е. обычная функция не может вызвать функцию с эффектами. +По аналогии с async/await, обычная функция не может вызвать функцию с эффектами. Например, вы не можете передавать функции с эффектами в `clojure.core/map`. +Подробности вы найдете в статье [What Color is Your Function?](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). + Есть надежда на то, что для JVM эту проблему решит [Project Loom](https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html). А пока вы можете воспользоваться функциями и макросами из [`darkleaf.effect.core-analogs`](test/darkleaf/effect/core_analogs_test.cljc). +## Async handlers + +Вы можете писать код с эффектами, синхронно его тестировать, но запускать с асинхронными обработчиками. +Для этого случая функция `e/perform` принимает дополнительные аргументы `respond` и `raise`. + +```clojure +(comment + (defn perform + ([handlers continuation coeffect-or-args]) + ([handlers continuation coeffect-or-args respond raise]))) +``` + +Асинхронный обработчик так же должнен принимать 2 дополнительных аргумента для ассинхронного случая + +```clojure +(comment + (defn my-identity-handler + ([x] x) + ([x respond raise] + (js/process.nextTick respond x)))) +``` + +## Exceptions + +## Higer-order effects ? + ## Middlewares Продолжение возвращает пару из эффекта и следующего продолжения. @@ -347,9 +379,16 @@ Expected effect сравнивается с actual effect по значению :return int?} :my/effect {:effect (fn [x] (int? x)) :coeffect int?}} + handlers {:my/effect (fn [x] + "wrong int")} continuation (-> effn (e/continuation) - (contract/wrap-contract contract 'my/effn))]) + (contract/wrap-contract contract 'my/effn))] + (try + (e/perform handlers continuation [1]) + (catch Throwable e + (ex-data e)))) +=> {:coeffect "wrong int", :path [:my/effect :coeffect]} ``` С помощью [log middleware](test/darkleaf/effect/middleware/log_test.cljc) @@ -357,30 +396,66 @@ Expected effect сравнивается с actual effect по значению Журнал может быть сериализован, передан на другую машину и применен для продолжения вычисления. Вы можете начать вычисление на сервере и продолжить его в браузере и наоборот. -Middleware можно объединять. Подробнее в [composition test](test/darkleaf/effect/middleware/composition_test.cljc). +```clojure +(require '[darkleaf.effect.middleware.log :as log]) +``` -## Async handlers +Чтобы функция прервала свое выполнения, обработчик должен вернуть особый коэффект - +`::log/suspend`. -Вы можете писать код с эффектами, синхронно его тестировать, но запускать с асинхронными обработчиками. -Для этого случая функция `e/perform` принимает дополнительные аргументы `respond` и `raise`. +```clojure +(def log-handlers {:my/suspend (fn [] ::log/suspend)}) + +(defn log-ef [x] + (with-effects + (+ x (! (effect :my/suspend))))) + +(def log-cont-1 (-> log-ef + (e/continuation) + (log/wrap-log))) + +(def log-suspended-result-1 (e/perform log-handlers log-cont-1 [1])) +``` + +`e/perform` вернет пару, где первый элемент сигнализирует о заморозке, а второй - +журнал выполненных эффектов и коэффектов. ```clojure -(comment - (defn perform - ([handlers continuation coeffect-or-args]) - ([handlers continuation coeffect-or-args respond raise]))) +log-suspended-result-1 +=> [::log/suspended [{:coeffect [1] ;; args + :next-effect [:my/suspend]}]] ``` -Асинхронный обработчик так же должнен принимать 2 дополнительных аргумента для ассинхронного случая +Чтобы продолжить выполнение, +нужно заново проиграть выполненные ранее эффекты с помощью `log/resume` +и передать вычисленный коэффект для последнего эффекта в журнале в `e/perform`. +В этом примере последний эффект - `:my/suspend`, а в качестве коэффекта пусть будет `2` ```clojure -(comment - (defn my-identity-handler - ([x] x) - ([x respond raise] - (js/process.nextTick respond x)))) +(def log-cont-2 (-> log-ef + (e/continuation) + (log/wrap-log) + (log/resume (last log-suspended-result-1)))) + +(def log-suspended-result-2 (e/perform log-handlers log-cont-2 2)) ``` +В итоге `e/perform` вернет тройку, где первый элемен сигнализирует о завершении вычисления, +второй содержит результат, а третий - весь журнал с начала вычисления. + +```clojure +log-suspended-result-2 +=> [::log/result + 3 + [{:coeffect [1] + :next-effect [:my/suspend]} + {:coeffect 2 + :next-effect 3}]] +``` + +Middleware можно комбинировать. +Подробнее в [composition test](test/darkleaf/effect/middleware/composition_test.cljc). + ## Internals https://github.com/leonoel/cloroutine From dda1a140bb27e7d6d6172193847ad2447474129e Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sun, 14 Jun 2020 21:13:11 +0400 Subject: [PATCH 10/16] save --- README.md | 83 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 602a293..ec721ae 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,8 @@ build_login_2 :: IO SessionData -> [Dependency rejection](https://blog.ploeh.dk/2017/02/02/dependency-rejection/). Стоит также рассмотреть зависимости между функциями. Есть 2 типа зависимостей: -compile time и run time. Суть внедрения зависимостей в инверсии compile time зависимостей, -т.е. в compile time оболочка зависит (реализует неявный интерфейс) от ядра, +compile time и run time. Суть внедрения зависимостей в инверсии compile time зависимостей, но не run time. +Т.е. в compile time оболочка зависит (реализует неявный интерфейс) от ядра, но в run time ядро зависит (вызывает) от оболочки. А как сделать так, чтобы императивная оболочка вызывала чистое ядро? @@ -124,9 +124,6 @@ compile time и run time. Суть внедрения зависимостей )])])])]) ``` -Т.е. `login-4` возвращает `[effect-description continuation-1]`, -первое продолжение в свою очередь возвращает эффект и второе продолжение. - Теперь мы можем написать императивный интерпретатор эффектов вызывающий нашу чистую функцию и ее чистые продолжения. @@ -135,8 +132,8 @@ compile time и run time. Суть внедрения зависимостей Если вы не знакомы с концепцией эффектов, то прочитайте [Algebraic Effects for the Rest of Us](https://overreacted.io/algebraic-effects-for-the-rest-of-us/) -Функция `login-4` выглядит устрашающе и порождает "callback hell'. -К счастью у нас есть макросы и мы можем скрыть эту деталь от пользователя. +Бессмысленно вручную писать функции такие как `login-4`. +К счастью у нас есть макросы и мы можем писать привычный последовательный код. ```clojure (require '[darkleaf.effect.core :as e :refer [effect with-effects !]]) @@ -144,7 +141,7 @@ compile time и run time. Суть внедрения зависимостей Макрос `with-effects` делает всю работу. В местах, помеченных `!` происходит разрыв функции. Функция `effect` показывает, что мы прерываемся на вызов эффекта. -Можно провести некоторую аналогию между `with-effects/!` и `async/await` или `core.async`. +Можно провести некоторую аналогию между `with-effects`/`!` и `async`/`await` или `go`/`login-2, login-3`. +Чтобы их протестировать, нам потребовались бы шпионы (spies) с изменяемым состоянием: ```clojure (require '[clojure.test :as t]) +``` + +```clojure +(t/deftest login-test + (let [spy-state (atom []) + make-spy (fn [spy-name ret] + (fn [& args] + (swap! spy-state conj {:name spy-name + :args args + :ret ret}) + ret))] + (binding [*get-user-by-login* (make-spy :get-user-by-login {:id 1, :password-digest "digest"}) + *get-session* (make-spy :get-session {}) + *update-session* (make-spy :update-session :unused) + *check-password* (make-spy :check-password true)] + (t/is (= {:type :processed} + (login {:login "joe" :password "secret"}))) + (t/is (= [{:name :get-session + :args nil :ret {}} + {:name :get-user-by-login + :args ["joe"] :ret {:id 1, :password-digest "digest"}} + {:name :check-password + :args ["secret" "digest"] :ret true} + {:name :update-session + :args [assoc :user-id 1] :ret :unused}] + @spy-state))))) +``` + +Это довольно простой случай. Но что, если бы `login` вызывал `*get-user-by-login*` несколько раз +с разными аргументами и ожидал разные return values? + +Этот тест можно считать чистым, т.к. он не изменяет окружение, хоть и имеет локальное изменяемое состояние. Но чтобы нивелировать не чистоту `login` пришлось добавить +изменяемое состояние в виде `spy-state`. + +Т.к. функция `login-5` чистая, то мы можем описать последовательность запрашиваемых эффектов +с помощью неизменяемых структур данных. + +```clojure (require '[darkleaf.effect.script :as script]) ``` @@ -210,6 +246,17 @@ compile time и run time. Суть внедрения зависимостей Сценарий проверят какие и в каком порядке были запрошены эффекты и какие коэффекты нужно передать обратно. +Например, если мы хотим смоделировать ситуацию, когда пользователь уже залогинен, +то мы передадим соответствующий коэффект: + +```clojure +{:effect [:get-session] + :coeffect {:user-id 1}} +``` + +Сценарий проверяется функцией `script/test`. +Подобно `t/is` она вызывает `t/do-report`. + Expected effect сравнивается с actual effect по значению с помощью `clojure.core/=`. Также скрипт может проверять брошенные исключения с помощью `:thrown` или обрывать проверку на заданном эффекте с помощью `:final-effect` @@ -223,7 +270,7 @@ Expected effect сравнивается с actual effect по значению ## Stack -Функция с эффектами может вызывать другие функции с эффектами или без +По аналогии с `await`, `!` может использоваться для вызова функций с эффектами или без: ```clojure (t/deftest stack-use-case @@ -256,18 +303,22 @@ Expected effect сравнивается с actual effect по значению ## Core analogs -По аналогии с async/await, обычная функция не может вызвать функцию с эффектами. +По аналогии с `async`/`await`, обычная функция не может вызвать функцию с эффектами. Например, вы не можете передавать функции с эффектами в `clojure.core/map`. Подробности вы найдете в статье [What Color is Your Function?](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). -Есть надежда на то, что для JVM эту проблему решит [Project Loom](https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html). +Есть надежда на то, что для JVM эту проблему решит [Project Loom](https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html). И мы забудем про макрос `with-effects`. А пока вы можете воспользоваться функциями и макросами из [`darkleaf.effect.core-analogs`](test/darkleaf/effect/core_analogs_test.cljc). ## Async handlers -Вы можете писать код с эффектами, синхронно его тестировать, но запускать с асинхронными обработчиками. -Для этого случая функция `e/perform` принимает дополнительные аргументы `respond` и `raise`. +Повторюсь, в отличие от внедрения зависимостей мы возвращаем эффект и продолжение. +Строго говоря функция уже завершилась и процессор может заниматься чем-то другим. +Это позволяет реализовать неблокирующую обработку эффектов для существующей фукнции с эффектами. + +Для этого случая функция `e/perform` принимает дополнительные аргументы `respond` и `raise` +для передачи результата или исключения соответственно. ```clojure (comment @@ -276,11 +327,11 @@ Expected effect сравнивается с actual effect по значению ([handlers continuation coeffect-or-args respond raise]))) ``` -Асинхронный обработчик так же должнен принимать 2 дополнительных аргумента для ассинхронного случая +Асинхронный обработчик так же должен принимать 2 дополнительных аргумента для ассинхронного случая ```clojure (comment - (defn my-identity-handler + (defn my-identity-effect-handler ([x] x) ([x respond raise] (js/process.nextTick respond x)))) From 54c9f7ea91532eea81b3abfb6821e7f75e69d0a3 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sun, 14 Jun 2020 21:47:19 +0400 Subject: [PATCH 11/16] save --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ec721ae..3d6874c 100644 --- a/README.md +++ b/README.md @@ -315,10 +315,10 @@ Expected effect сравнивается с actual effect по значению Повторюсь, в отличие от внедрения зависимостей мы возвращаем эффект и продолжение. Строго говоря функция уже завершилась и процессор может заниматься чем-то другим. -Это позволяет реализовать неблокирующую обработку эффектов для существующей фукнции с эффектами. +Это позволяет реализовать неблокирующую обработку эффектов для существующей функции с эффектами. Для этого случая функция `e/perform` принимает дополнительные аргументы `respond` и `raise` -для передачи результата или исключения соответственно. +для передачи результата или исключения соответственно. Это работает как Clojure, так и в ClojureScript. ```clojure (comment From 1b2a4b25d1f0950067e70720a0c9a5fc3dc1eb64 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Mon, 15 Jun 2020 23:36:04 +0400 Subject: [PATCH 12/16] save --- README.md | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3d6874c..5af001b 100644 --- a/README.md +++ b/README.md @@ -339,7 +339,100 @@ Expected effect сравнивается с actual effect по значению ## Exceptions -## Higer-order effects ? +Исключения работают так, как вы ожидаете. +Например, если обработчик бросил исключение, то поймать его можно в функции с эффектами и принять нужное решение. + +```clojure +(defn catch-exception [] + (with-effects + (try + (! (effect :prn "Hi")) + (catch Throwable ex + (ex-message ex))))) +``` + +```clojure +(let [continuation (e/continuation catch-exception) + handlers {:prn (fn [_] + (throw (ex-info "Test" {})))}] + (e/perform handlers continuation [])) +=> "Test" +``` + +Чтобы протестировать обработку исключения, передайте его как coeffect: + +```clojure +(t/deftest catch-exception-test + (let [continuation (e/continuation catch-exception) + script [{:args []} + {:effect [:prn "Hi"] + :coeffect (ex-info "Test" {})} + {:return "Test"}]] + (script/test continuation script))) +``` + + +С помощью `thrown` можно проверить какое исключение было брошено: + +```clojure +(t/deftest throw-exception-test + (let [ef (fn [] + (with-effects + (throw (ex-info "Message" {:foo :bar})))) + continuation (e/continuation ef) + script [{:args []} + {:effect [:some-eff] + :coeffect :some-coeff} + {:thrown {:type ExceptionInfo + :message "Message" + :data {:foo :bar}}}]] + (script/test continuation script))) +``` + +## Effect as value + +`effect` - обычная фукнция и может использоваться отдельно от `!`. + +```clojure +(t/deftest effect-as-value + (let [effect-tag :prn + effect-arg 1 + test-effect (effect effect-tag effect-arg) + ef (fn [] + (with-effects + (! test-effect))) + continuation (e/continuation ef) + script [{:args []} + {:effect [:prn 1] + :coeffect nil} + {:return nil}]] + (script/test continuation script))) +``` + +## Higer order effect + +Эффект - это значение и функции могут возвращать эффект так же как и любое другое значение. + +```clojure +(t/deftest higher-order-effect + (let [nested-ef (fn [] + (with-effects + (! (effect :a)) + (effect :b))) + ef (fn [] + (with-effects + (! (! (nested-ef))))) + ;; ----^ runs effect [:b] + ;; -------^ runs nested-ef + continuation (e/continuation ef) + script [{:args []} + {:effect [:a] + :coeffect nil} + {:effect [:b] + :coeffect :some-value} + {:return :some-value}]] + (script/test continuation script))) +``` ## Middlewares @@ -509,4 +602,19 @@ Middleware можно комбинировать. ## Internals -https://github.com/leonoel/cloroutine +Макрос `with-effects` использует библиотеку [cloroutine](https://github.com/leonoel/cloroutine) +для преобразования форм в стейт машину. + +Как показано раньше, континуация - это обычная функция и она ожидаемо может быть вызвана много раз. +Это называется multi-shot континуацией. + +```clojure +(defn login-4 [{:as form :keys [login password]}] + [[:get-session] + (fn [session] + ;;... + )]) +``` + +Однако, сloroutine предоставляет только one-shot корутины и механизм их клонирования. +Это позволяет реализовать ожидаемое multi-shot поведение. From 3c7f81c047bbe9166877a11a94bc8a83c3da6a7b Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Mon, 15 Jun 2020 23:40:03 +0400 Subject: [PATCH 13/16] typos --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5af001b..f1aa3d2 100644 --- a/README.md +++ b/README.md @@ -327,7 +327,7 @@ Expected effect сравнивается с actual effect по значению ([handlers continuation coeffect-or-args respond raise]))) ``` -Асинхронный обработчик так же должен принимать 2 дополнительных аргумента для ассинхронного случая +Асинхронный обработчик так же должен принимать 2 дополнительных аргумента для асинхронного случая ```clojure (comment @@ -391,7 +391,7 @@ Expected effect сравнивается с actual effect по значению ## Effect as value -`effect` - обычная фукнция и может использоваться отдельно от `!`. +`effect` - обычная функция и может использоваться отдельно от `!`. ```clojure (t/deftest effect-as-value @@ -409,7 +409,7 @@ Expected effect сравнивается с actual effect по значению (script/test continuation script))) ``` -## Higer order effect +## Higher order effect Эффект - это значение и функции могут возвращать эффект так же как и любое другое значение. @@ -584,7 +584,7 @@ log-suspended-result-1 (def log-suspended-result-2 (e/perform log-handlers log-cont-2 2)) ``` -В итоге `e/perform` вернет тройку, где первый элемен сигнализирует о завершении вычисления, +В итоге `e/perform` вернет тройку, где первый элемент сигнализирует о завершении вычисления, второй содержит результат, а третий - весь журнал с начала вычисления. ```clojure From c3fda8cfc1d0ad9c68df24448541433482d4e77b Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Mon, 15 Jun 2020 23:41:56 +0400 Subject: [PATCH 14/16] headers --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index f1aa3d2..4f0dabf 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,8 @@ Expected effect сравнивается с actual effect по значению #_"some code") ``` +### Context + С помощью [context middleware](test/darkleaf/effect/middleware/context_test.cljc) вы можете передавать контекст между обработчиками эффектов. Это напоминает монады State, Reader и Writer. @@ -482,6 +484,8 @@ Expected effect сравнивается с actual effect по значению После применения `context/wrap-context` обработчики принимают контекст первым дополнительным аргументом и должны возвращать пару из контекста и коэффекта. +### Reduced + С помощью [reduced middleware](test/darkleaf/effect/middleware/reduced_test.cljc) вы можете досрочно прервать вычисление. Это напоминает монады Maybe или Either. @@ -508,6 +512,8 @@ Expected effect сравнивается с actual effect по значению Если обработчик возвращает `reduced` значение, то вычисление прерывается и это значение используется для возврата из функции. +### Contract + С помощью [contract middleware](test/darkleaf/effect/middleware/contract_test.cljc) вы можете проверять контракты функций и их эффектов/коэффектов. @@ -535,6 +541,8 @@ Expected effect сравнивается с actual effect по значению => {:coeffect "wrong int", :path [:my/effect :coeffect]} ``` +### Log + С помощью [log middleware](test/darkleaf/effect/middleware/log_test.cljc) вы можете вести журнал эффектов, что позволяет замораживать и продолжать вычисление. Журнал может быть сериализован, передан на другую машину и применен для продолжения вычисления. From 5b9358b9f0af24ab5db0bb7cd23c3dd85328a10f Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Mon, 15 Jun 2020 23:44:05 +0400 Subject: [PATCH 15/16] TOC --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 4f0dabf..a101422 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,23 @@ Алгебраические эффекты для Clojure(Script). +- [Api](#api) +- [Rationale](#rationale) +- [Effect](#effect) + * [Script testing](#script-testing) + * [Stack](#stack) + * [Core analogs](#core-analogs) + * [Async handlers](#async-handlers) + * [Exceptions](#exceptions) + * [Effect as value](#effect-as-value) + * [Higher order effect](#higher-order-effect) + * [Middlewares](#middlewares) + + [Context](#context) + + [Reduced](#reduced) + + [Contract](#contract) + + [Log](#log) + * [Internals](#internals) + # Api * [core test](test/darkleaf/effect/core_test.cljc). From 268a0aafdec15318a81db72b4233df71f5d9171c Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Mon, 15 Jun 2020 23:45:40 +0400 Subject: [PATCH 16/16] api --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a101422..a039adc 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,16 @@ # Api -* [core test](test/darkleaf/effect/core_test.cljc). -* [script test](test/darkleaf/effect/script_test.cljc). -* [core analogs test](test/darkleaf/effect/core_analogs_test.cljc). -* middleware - * [composition test](test/darkleaf/effect/middleware/composition_test.cljc). - * [context test](test/darkleaf/effect/middleware/context_test.cljc). - * [contract test](test/darkleaf/effect/middleware/contract_test.cljc). - * [log test](test/darkleaf/effect/middleware/log_test.cljc). - * [reduced test](test/darkleaf/effect/middleware/reduced_test.cljc). +Исчерпывающее описание api вы сможете найти в тестах: + +* [darkleaf.effect.core-test](test/darkleaf/effect/core_test.cljc) +* [darkleaf.effect.script-test](test/darkleaf/effect/script_test.cljc) +* [darkleaf.effect.core-analogs-test](test/darkleaf/effect/core_analogs_test.cljc) +* [darkleaf.effect.middleware.composition-test](test/darkleaf/effect/middleware/composition_test.cljc) +* [darkleaf.effect.middleware.context-test](test/darkleaf/effect/middleware/context_test.cljc) +* [darkleaf.effect.middleware.contract-test](test/darkleaf/effect/middleware/contract_test.cljc) +* [darkleaf.effect.middleware.log-test](test/darkleaf/effect/middleware/log_test.cljc) +* [darkleaf.effect.middleware.reduced-test](test/darkleaf/effect/middleware/reduced_test.cljc) # Rationale