Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add manifold applicative engine #14

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
11 changes: 9 additions & 2 deletions src/nodely/api/v0.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)))
Expand Down
70 changes: 70 additions & 0 deletions src/nodely/engine/applicative/manifold.clj
Original file line number Diff line number Diff line change
@@ -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)))

;
)
15 changes: 15 additions & 0 deletions test/nodely/api_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
#{:core-async.lazy-scheduling
:core-async.iterative-scheduling
:async.manifold
:applicative.manifold
:applicative.promesa
:applicative.core-async
:applicative.virtual-future
Expand Down Expand Up @@ -127,6 +128,20 @@
(t/matching
5
(async/<!! (api/eval-key-channel env :z {::api/engine :core-async.lazy-scheduling}))))))
(testing-require-delay
nodely.engine.applicative.manifold nodely.api.v0/manifold-failure
"Kaboom! We don't have manifold for pretend" :test-manifold-failure
(t/testing "without manifold on the classpath"
(t/testing "attempting to use manifold"
(t/matching
#"Could not locate manifold on classpath"
(try (api/eval-key-channel env :z {::api/engine :applicative.manifold})
(catch Throwable t
(ex-message t)))))
(t/testing "attempting to use core.async"
(t/matching
5
(async/<!! (api/eval-key-channel env :z {::api/engine :core-async.lazy-scheduling}))))))
(testing-require-delay
nodely.engine.applicative.promesa nodely.api.v0/promesa-failure
"Kaboom! We don't have promesa for pretend" :test-promesa-failure
Expand Down
75 changes: 74 additions & 1 deletion test/nodely/engine/applicative_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
[clojure.test.check.properties :as prop]
[criterium.core :as criterium]
[matcher-combinators.matchers :as matchers]
[matcher-combinators.test :refer [match?]]
[matcher-combinators.test :refer [match? thrown-match?]]
[nodely.data :as data]
[nodely.engine.applicative :as applicative]
[nodely.engine.applicative.core-async :as core-async]
[nodely.engine.applicative.manifold :as manifold]
[nodely.engine.applicative.promesa :as promesa]
[nodely.engine.applicative.synchronous :as synchronous]
[nodely.engine.core :as core]
Expand Down Expand Up @@ -307,3 +308,75 @@
promesa/context java.util.concurrent.CompletableFuture
core-async/context clojure.core.async.impl.channels.ManyToManyChannel
synchronous/context nodely.engine.applicative.synchronous.Box))

(deftest manifold-applicative-test
(let [simple-env {:a (>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})))))
8 changes: 4 additions & 4 deletions test/nodely/engine/manifold_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,21 @@
(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
(testing "async response is equal to sync response"
(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))))
Expand Down
Loading