Алгебраические эффекты

  • Тестирование кода с эффектами
  • Тестирование ассинхронного кода синхронно
  • Сохранение и восстановление вычисления
  • Корутины

Как тестировать?


            (defn new-article [title body]
              {:id         (java.util.UUID/randomUUID)
               :created-at (java.time.Instant/now)
               :title      title
               :body       body})

            (new-article "Lorem ipsum" "dolor sit amet...")
          

Жадная чистая функция


              (defn new-article [id created-at title body]
                {:id         id
                 :created-at created-at
                 :title      title
                 :body       body})

              (let [new-article* (partial new-article
                                          (java.util.UUID/randomUUID)
                                          (java.time.Instant/now))]
                (new-article* "Lorem ipsum" "dolor sit amet..."))
            

Ненужное значение


                (defn new-article [id created-at value title body]
                  {:id         id
                   :created-at created-at
                   :title      title
                   :body       body
                   :answer     (if (= "The Hitchhiker's Guide..." title)
                                 value
                                 0)})
            

ООП


                (defn new-article-factory [get-id get-instant]
                  (fn [title body]
                    {:id         (get-id)
                     :created-at (get-instant)
                     :title      title
                     :body       body}))

                (let [get-id      #(java.util.UUID/randomUUID)
                      get-instant #(java.time.Instant/now)
                      new-article (new-article-factory get-id get-instant)]
                  (new-article "Lorem ipsum" "dolor sit amet..."))
            

Сложно тестировать порядок вызовов


                (let [log         (atom [])
                      get-id      #(do
                                     (swap! log conj :get-id)
                                     (java.util.UUID/randomUUID))
                      get-instant #(do
                                     (swap! log conj :get-instant)
                                     (java.time.Instant/now))
                      new-article (new-article-factory get-id get-instant)]
                  (new-article "Lorem ipsum" "dolor sit amet...")
                  (assert (= [:get-id :get-instant] @log)))
            

Контекст


              (declare ^:dynamic *get-id*
                       ^:dynamic *get-instant*)

              (defn new-article [title body]
                {:id         (*get-id*)
                 :created-at (*get-instant*)
                 :title      title
                 :body       body})

              (binding [*get-id*      #(java.util.UUID/randomUUID)
                        *get-instant* #(java.time.Instant/now)]
                (new-article "Lorem ipsum" "dolor sit amet..."))
          

Эффект и продолжение


              (defn new-article [title body]
                [:get-id (fn [id]
                           [:get-instant (fn [created-at]
                                           {:id         id
                                            :created-at created-at
                                            :title      title
                                            :body       body})])])

              (let [[ef-1 cont-1] (new-article "Lorem ipsum"
                                               "dolor sit amet...")
                    _             (assert (= :get-id ef-1))
                    [ef-2 cont-2] (cont-1 (java.util.UUID/randomUUID))
                    _             (assert (= :get-instant ef-2))]
                (cont-2 (java.time.Instant/now)))
          

darkleaf/effect


                (defn example-fn [x]
                  (with-effects
                    (let [rnd (! (effect [:random]))]
                      (- (* 2. x rnd) x))))

                (def continuation (e/continuation example-fn))
            

Макрос with-effects

  • leonoel/cloroutine
  • возвращает stackless one-shot корутину
  • преобразует код в SSA форму

Функция e/continuation

  • добавляет поддежку стека
  • multi-shot

Функция !


                ;; эффект
                (! (effect [:some-effect :arg])
                (! (effect `any-value-with-metadata-support))
            

                ;; вызов функции с эффектами
                (! (some-fn! :val))
            

                ;; простое значения
                (! 42)
                (! [:some-value])
            

                ;; вызов обычных функций
                (! (inc 41))
            

                ;; эффект как значение
                (let [my-effect (effect [:some-effect])]
                  (! my-effect)
            

real example


              (defn user-registration []
                (with-effects
                  (if (-> (! (effect [:session/get]))
                          :current-user-id some?)
                    (! (effect [:ui.screen/show :main]))
                    (loop [user (make-user)]
                      (let [user (! (effect [:ui.form/edit user]))
                            user (validate form)]
                        (if (has-errors? user)
                          (recur user)
                          (do
                            ;; ...
                            (! (effect [:persistence/create user])))))))))
          

re-frame


              (reg-event-fx
                :my-event
                (fn [cofx [_ a]]
                  {:db       (assoc (:db cofx) :flag  a)
                   :dispatch [:do-something-else 3]}))
          
  • жадное вычисление коэффектов
  • единый процесс теряется среди разрозненных событий

Interpretator


              (defn example-fn [x]
                (with-effects
                  (let [rnd (! effect [:random]))]
                    (- (* 2. x rnd) x))))

              (defn effect-!>coeffect [effect]
                (match effect [:random] 0.75))

              (def continuation (e/continuation example-fn))

              (defn example-fn* [x]
                (e/perform effect-!>coeffect continuation [x]))

              (assert (= 0.5 (example-fn* 1)))
          

Script


              (defn example-fn [x]
                (with-effects
                  (let [rnd (! effect [:random]))]
                    (- (* 2. x rnd) x))))

              (deftest example-fn-test
                (let [continuation (e/continuation example-fn)
                      script [{:args     [1]}
                              {:effect   [:random]
                               :coeffect 0.75}
                              {:return   0.5}]]
                  (script/test continuation script))))))
          

core-analogs

  • reduce!
  • mapv!
  • ->!
  • ->>!

Sync & Async


              (defn effect-!>coeffect [effect]
                (match effect
                       [:random] 0.75))

              (defn f [x]
                (e/perform effect-!>coeffect continuation [x]))
          

              (defn effect-!>coeffect [effect respond raise]
                (match effect
                       [:random] (next-tick respond 0.75)))

              (defn f [x respond raise]
                (e/perform effect-!>coeffect
                           continuation
                           [x]
                           respond raise))
          

Middleware


              (defn wrap-blank [continuation]
                (when (some? continuation)
                  (fn [coeffect]
                    (let [[effect continuation] (continuation coeffect)]
                      [effect (wrap-blank continuation)]))))

              (def continuation (-> (e/continuation example-fn)
                                    (wrap-blank)))
          
darkleaf.effect.middleware.context

              (defn state-example []
                (with-effects
                  [(! (effect [:update inc]))
                   (! (effect [:update + 2]))
                   (! (effect [:get]))]))
          

              (defn effect-!>coeffect [[context effect]]
                (match effect
                       [:get]
                       [context (:state context)]

                       [:update f & args]
                       (let [context (apply update context :state f args)]
                         [context (:state context)])))
          
darkleaf.effect.middleware.reduced

              (defn maybe-example [x]
                (with-effects
                  (+ 5 (! (effect [:maybe x])))))
          

              (defn effect->coeffect [effect]
                (match effect
                       [:maybe nil] (reduced nil)
                       [:maybe val] val))
          
darkleaf.effect.middleware.log

              (defn suspend-resume-example [x]
                (with-effects
                  (let [a (! (effect [:suspend]))]
                    ...)))
          

              (defn effect-!>coeffect [effect]
                (match effect
                       [:effect]  :coeffect
                       [:suspend] ::log/suspend))

              (def continuation (-> (e/continuation suspend-resume-example)
                                    (log/wrap-log)))

              (assert (= [::log/suspended [{:coeffect    [:arg]
                                            :next-effect [:suspend]}]]
                         (e/perform effect-!>coeffect continuation [:arg])

              (def continuation (-> (e/continuation ef)
                                    (log/wrap-log)
                                    (log/resume log))
          
  • functional core & imperative shell
  • логика в cljc, тесты в clj, оболочка в cljs
  • нет дробления процесса на фукнции/события
  • можно передавать вычисление в браузер и обратно
github.com/darkleaf/effect