diff --git a/README.md b/README.md index 2001d3d..a298a5c 100644 --- a/README.md +++ b/README.md @@ -214,10 +214,11 @@ Engines supported in Nodely include: | ------------------------------- | ---------------------------------- | ------------ | | Lazy Synchronous | `:sync.lazy` | Mature | | Core Async Lazy Scheduling | `:core-async.lazy-scheduling` | Mature | - | Applicative Virtual Threads | `:applicative.virtual-futures` | Mature | + | Applicative Virtual Threads | `:applicative.virtual-future` | Mature | | Async Virtual Threads | `:async.virtual-futures` | Experimental | | Core Async Iterative Scheduling | `:core-async.iterative-scheduling` | Experimental | | Async Manifold | `:async.manifold` | Experimental | + | Async Manifold Applicative | `:applicative.manifold` | Experimental | | Async Applicative | `:applicative.core-async` | Experimental | | Promesa Async Applicative | `:applicative.promesa` | Experimental | diff --git a/project.clj b/project.clj index 46d7758..f7bec9e 100644 --- a/project.clj +++ b/project.clj @@ -10,7 +10,7 @@ [aysylu/loom "1.0.2"] [org.clojure/core.async "1.5.648" :scope "provided"] [funcool/promesa "10.0.594" :scope "provided"] - [manifold "0.1.9-alpha5" :scope "provided"] + [manifold "0.4.3" :scope "provided"] [prismatic/schema "1.1.12"]] :exclusions [log4j] diff --git a/src/nodely/api/v0.clj b/src/nodely/api/v0.clj index 90c6c05..0c803ba 100644 --- a/src/nodely/api/v0.clj +++ b/src/nodely/api/v0.clj @@ -54,11 +54,13 @@ (def manifold-failure (delay - (try (require 'nodely.engine.manifold) + (try (require 'nodely.engine.manifold + 'nodely.engine.applicative.manifold) (catch Exception e {:msg "Could not locate manifold on classpath." ::error :missing-ns - ::requested-namespaces '[nodely.engine.manifold] + ::requested-namespaces '[nodely.engine.manifold + nodely.engine.applicative.manifold] :cause e})))) (def promesa-failure @@ -81,6 +83,11 @@ :async.manifold {::ns-name 'nodely.engine.manifold ::opts-fn (constantly nil) ::enable-deref manifold-failure} + :applicative.manifold {::ns-name 'nodely.engine.applicative + ::opts-fn #(assoc % ::applicative/context + (var-get (resolve 'nodely.engine.applicative.manifold/context))) + ::eval-key-channel true + ::enable-deref manifold-failure} :applicative.promesa {::ns-name 'nodely.engine.applicative ::opts-fn #(assoc % ::applicative/context (var-get (resolve 'nodely.engine.applicative.promesa/context))) diff --git a/src/nodely/engine/applicative/manifold.clj b/src/nodely/engine/applicative/manifold.clj new file mode 100644 index 0000000..f0ad0b2 --- /dev/null +++ b/src/nodely/engine/applicative/manifold.clj @@ -0,0 +1,70 @@ +(ns nodely.engine.applicative.manifold + (:require + [manifold.deferred :as deferred] + [nodely.engine.applicative.protocols :as protocols])) + +(declare context) + +(defn deref-unwrapped + [it] + (try (deref it) + (catch java.util.concurrent.ExecutionException e + (throw (.getCause e))))) + +(extend-type manifold.deferred.Deferred + protocols/Contextual + (-get-context [_] context) + + protocols/Extract + (-extract [it] + (deref-unwrapped it))) + +(def context + (reify + protocols/RunNode + (-apply-fn [_ f mv] + (deferred/future (f (deref-unwrapped mv)))) + + protocols/Functor + (-fmap [_ f mv] + (deferred/future (f (deref-unwrapped mv)))) + + protocols/Monad + (-mreturn [_ v] + (deferred/future v)) + + (-mbind [_ mv f] + (deferred/future (let [v (deref-unwrapped mv)] + (deref-unwrapped (f v))))) + + protocols/Applicative + (-pure [_ v] + (deferred/future v)) + + (-fapply [_ pf pv] + (deferred/future (let [f (deref-unwrapped pf) + v (deref-unwrapped pv)] + (f v)))))) + +(comment + (def subscribe + (fn + ([this d x f] + (let [d (or d (deferred/deferred))] + (deferred/on-realized x + #(this d % f) + #(deferred/error! d %)) + d)))) + + (defn mbind [mv f] + (let [d (deferred/deferred)] + (deferred/on-realized + mv + #(deferred/success! d (f %)) + #(deferred/error! d %)) + d)) + + (mbind (deferred/future 1) (fn [x] (Thread/sleep 1000) (inc x))) + + ; + ) diff --git a/test/nodely/api_test.clj b/test/nodely/api_test.clj index ccc50de..8517159 100644 --- a/test/nodely/api_test.clj +++ b/test/nodely/api_test.clj @@ -50,6 +50,7 @@ #{:core-async.lazy-scheduling :core-async.iterative-scheduling :async.manifold + :applicative.manifold :applicative.promesa :applicative.core-async :applicative.virtual-future @@ -127,6 +128,20 @@ (t/matching 5 (async/value 2) + :b (>value 1) + :c (>leaf (+ ?a ?b))} + env-with-failing-schema {:a (>value 2) + :b (>value 1) + :c (yielding-schema (>leaf (+ ?a ?b)) s/Bool)}] + (testing "it should not fail" + (is (match? 3 (applicative/eval-key simple-env :c {::applicative/context manifold/context})))) + + (testing "more complicated example" + (is (match? 4 (applicative/eval-key tricky-example :z {::applicative/context manifold/context})))) + + (testing "returns ex-info when schema is selected as fvalidate, and schema fn validation is enabled" + (is (thrown-match? clojure.lang.ExceptionInfo + {:type :schema.core/error + :schema java.lang.Boolean + :value 3} + (ex-data + (s/with-fn-validation + (applicative/eval-key env-with-failing-schema :c {::applicative/fvalidate schema/fvalidate + ::applicative/context manifold/context})))))))) + +(deftest manifold-eval-key-test + (testing "eval promise" + (is (match? 3 (applicative/eval-key test-env :c {::applicative/context manifold/context})))) + (testing "async works" + (let [[time-ns result] (criterium/time-body (applicative/eval-key test-env+delay-core-async + :d + {::applicative/context manifold/context}))] + (is (match? {:a 3 :b 6 :c 9} result)) + (is (match? (matchers/within-delta 100000000 1000000000) time-ns)))) + (testing "tricky example" + (is (match? 4 (applicative/eval-key tricky-example :z + {::applicative/context manifold/context}))))) + +(deftest manifold-eval-test + (testing "eval promise" + (is (match? {:a {::data/value 2} + :b {::data/value 1} + :c {::data/value 3}} + (applicative/eval test-env :c {::applicative/context manifold/context})))) + (testing "tricky example" + (is (match? {:x (data/value 1) + :y (data/value 2) + :a (data/value 3) + :b (data/value 4) + :c (data/value 5) + :w (data/value 4) + :z {::data/type :leaf + ::data/inputs #{:w}}} + (applicative/eval tricky-example :w {::applicative/context manifold/context}))))) + +(deftest manifold-eval-env-with-sequence + (testing "async response is equal to sync response" + (is (match? (-> (core/resolve :b env-with-sequence) (get :b) ::data/value) + (applicative/eval-key env-with-sequence :b {::applicative/context manifold/context})))) + (testing "sync=async for sequence with nil values" + (is (match? (-> (core/resolve :b env+sequence-with-nil-values) (get :b) ::data/value) + (applicative/eval-key env+sequence-with-nil-values :b {::applicative/context manifold/context})))) + (testing "sync=async for sequence returning nil values" + (is (match? (-> (core/resolve :b env+sequence-returning-nil-values) (get :b) ::data/value) + (applicative/eval-key env+sequence-returning-nil-values :b {::applicative/context manifold/context})))) + (testing "async version takes a third of the time of sync version + (runtime diff is 2 sec, within a tolerance of 10ms" + (let [[nanosec-sync _] (criterium/time-body (core/resolve :c env-with-sequence+delay-sync)) + [nanosec-async _] (criterium/time-body (applicative/eval-key env-with-sequence+delay-sync :c {::applicative/context manifold/context}))] + (is (match? (matchers/within-delta 10000000 2000000000) + (- nanosec-sync nanosec-async))))) + (testing "Actually computes the correct answers" + (is (match? [2 3 4] (applicative/eval-key env-with-sequence+delay-sync :c {::applicative/context manifold/context}))))) diff --git a/test/nodely/engine/manifold_test.clj b/test/nodely/engine/manifold_test.clj index 0515d9b..c8bacca 100644 --- a/test/nodely/engine/manifold_test.clj +++ b/test/nodely/engine/manifold_test.clj @@ -42,10 +42,10 @@ (is (core/resolve :d test-env) (manifold/eval-env test-env))) (testing "async version takes half the time of sync version - (runtime diff is 1 sec, within a tolerance of 3ms" + (runtime diff is 1 sec, within a tolerance of 10ms" (let [[nanosec-sync _] (time-body (core/resolve :d test-env+delay)) [nanosec-async _] (time-body (manifold/eval-env test-env+delay))] - (is (match? (matchers/within-delta 6000000 1000000000) + (is (match? (matchers/within-delta 10000000 1000000000) (- nanosec-sync nanosec-async)))))) (deftest eval-env-with-sequence @@ -53,10 +53,10 @@ (is (core/resolve :b env-with-sequence) (manifold/eval-env env-with-sequence))) (testing "async version takes a third of the time of sync version - (runtime diff is 2 sec, within a tolerance of 3ms" + (runtime diff is 2 sec, within a tolerance of 10ms" (let [[nanosec-sync _] (time-body (core/resolve :b env-with-sequence+delay)) [nanosec-async _] (time-body (manifold/eval-env env-with-sequence+delay))] - (is (match? (matchers/within-delta 8000000 2000000000) + (is (match? (matchers/within-delta 10000000 2000000000) (- nanosec-sync nanosec-async))))) (testing "Actually computes the correct answers" (is (= [2 3 4] (manifold/eval-key env-with-sequence+delay :b))))