diff --git a/scripts/benchmarks.clj b/scripts/benchmarks.clj index 35484b0..e806dc0 100644 --- a/scripts/benchmarks.clj +++ b/scripts/benchmarks.clj @@ -1,6 +1,7 @@ (ns com.rpl.specter.benchmarks (:use [com.rpl.specter] [com.rpl.specter macros] + [com.rpl.specter.transient] [com.rpl.specter.impl :only [benchmark]]) (:require [clojure.walk :as walk])) @@ -153,3 +154,46 @@ (tree-value-transform (fn [e] (if (even? e) (inc e) e)) data) )) +(run-benchmark "transient comparison: building up vectors" + 6 + 10000 + (reduce (fn [v i] (conj v i)) [] (range 1000)) + (reduce (fn [v i] (conj! v i)) (transient []) (range 1000)) + ;; uncomment this when END is fast + #_(reduce (fn [v i] (setval [END] [i] v)) [] (range 1000)) + (setval [END!] (range 1000) (transient [])) + (reduce (fn [v i] (setval [END!] [i] v)) (transient []) (range 1000))) + +(let [data (vec (range 1000)) + tdata (transient data)] + (run-benchmark "transient comparison: assoc'ing in vectors" + 6 + 500000 + (assoc data (rand-int 1000) 0) + (assoc! tdata (rand-int 1000) 0) + (setval [(keypath (rand-int 1000))] 0 data) + (setval [(keypath! (rand-int 1000))] 0 tdata))) + +(let [data (into {} (for [k (range 1000)] + [k (rand)])) + tdata (transient data)] + (run-benchmark "transient comparison: assoc'ing in maps" + 6 + 500000 + (assoc data (rand-int 1000) 0) + (assoc! tdata (rand-int 1000) 0) + (setval [(keypath (rand-int 1000))] 0 data) + (setval [(keypath! (rand-int 1000))] 0 tdata))) + +(defn modify-submap + [m] + (assoc m 0 1 458 89)) + +(let [data (into {} (for [k (range 1000)] + [k (rand)])) + tdata (transient data)] + (run-benchmark "transient comparison: submap" + 6 + 300000 + (transform [(submap [(rand-int 1000)])] modify-submap data) + (transform [(submap! [(rand-int 1000)])] modify-submap tdata))) diff --git a/src/clj/com/rpl/specter/impl.cljx b/src/clj/com/rpl/specter/impl.cljx index b401e79..3f1b3ee 100644 --- a/src/clj/com/rpl/specter/impl.cljx +++ b/src/clj/com/rpl/specter/impl.cljx @@ -497,6 +497,14 @@ (defn vec-count [v] (count v)) +#+clj +(defn transient-vec-count [^clojure.lang.ITransientVector v] + (.count v)) + +#+cljs +(defn transient-vec-count [v] + (count v)) + (extend-protocol UpdateExtremes #+clj clojure.lang.PersistentVector #+cljs cljs.core/PersistentVector (update-first [v afn] @@ -536,6 +544,9 @@ #+clj clojure.lang.IPersistentVector #+cljs cljs.core/PersistentVector (fast-empty? [v] (= 0 (vec-count v))) + #+clj clojure.lang.ITransientVector #+cljs cljs.core/TransientVector + (fast-empty? [v] + (= 0 (transient-vec-count v))) #+clj Object #+cljs default (fast-empty? [s] (empty? s)) @@ -831,6 +842,16 @@ (next-fn structure) )) +(deftype TransientEndNavigator []) + +(extend-protocol p/Navigator + TransientEndNavigator + (select* [this structure next-fn] + (next-fn [])) + (transform* [this structure next-fn] + (let [res (next-fn [])] + (reduce conj! structure res)))) + (defn extract-basic-filter-fn [path] (cond (fn? path) path diff --git a/src/clj/com/rpl/specter/transient.cljx b/src/clj/com/rpl/specter/transient.cljx new file mode 100644 index 0000000..774e19f --- /dev/null +++ b/src/clj/com/rpl/specter/transient.cljx @@ -0,0 +1,91 @@ +(ns com.rpl.specter.transient + #+cljs + (:require-macros [com.rpl.specter.macros + :refer + [defnav + defpathedfn]]) + (:use #+clj + [com.rpl.specter.macros :only + [defnav + defpathedfn]]) + (:require [com.rpl.specter.impl :as i] + [com.rpl.specter :refer [subselect selected?]])) + +(defnav + ^{:doc "Navigates to the specified key of a transient collection, + navigating to nil if it doesn't exist."} + keypath! + [key] + (select* [this structure next-fn] + (next-fn (get structure key))) + (transform* [this structure next-fn] + (assoc! structure key (next-fn (get structure key))))) + +(def END! + "Navigates to an empty (persistent) vector at the end of a transient vector." + (i/comp-paths* [(i/->TransientEndNavigator)])) + +(defn- t-get-first + [tv] + (nth tv 0)) + +(defn- t-get-last + [tv] + (nth tv (dec (i/transient-vec-count tv)))) + +(defn- t-update-first + [tv next-fn] + (assoc! tv 0 (next-fn (nth tv 0)))) + +(defn- t-update-last + [tv next-fn] + (let [i (dec (i/transient-vec-count tv))] + (assoc! tv i (next-fn (nth tv i))))) + +(def FIRST! + "Navigates to the first element of a transient vector." + (i/->PosNavigator t-get-first t-update-first)) + +(def LAST! + "Navigates to the last element of a transient vector." + (i/->PosNavigator t-get-last t-update-last)) + +#+clj +(defn- select-keys-from-transient-map + "Selects keys from transient map, because built-in select-keys uses + `find` which is unsupported." + [m m-keys] + (loop [result {} + m-keys m-keys] + (if-not (seq m-keys) + result + (let [k (first m-keys) + ;; support Clojure 1.6 where contains? is broken on transients + item (get m k ::not-found)] + (recur (if-not (identical? item ::not-found) + (assoc result k item) + result) + (rest m-keys)))))) + +#+cljs +(defn- select-keys-from-transient-map + "Uses select-keys on a transient map." + [m m-keys] + (select-keys m m-keys)) + +(defnav + ^{:doc "Navigates to the specified persistent submap of a transient map."} + submap! + [m-keys] + (select* [this structure next-fn] + (next-fn (select-keys-from-transient-map structure m-keys))) + (transform* [this structure next-fn] + (let [selected (select-keys-from-transient-map structure m-keys) + res (next-fn selected)] + (as-> structure % + (reduce (fn [m k] + (dissoc! m k)) + % m-keys) + (reduce-kv (fn [m k v] + (assoc! m k v)) + % res))))) diff --git a/test/com/rpl/specter/core_test.cljx b/test/com/rpl/specter/core_test.cljx index 64acbc4..9aa1510 100644 --- a/test/com/rpl/specter/core_test.cljx +++ b/test/com/rpl/specter/core_test.cljx @@ -25,6 +25,7 @@ #+cljs [cljs.test.check.generators :as gen] #+cljs [cljs.test.check.properties :as prop :include-macros true] [com.rpl.specter :as s] + [com.rpl.specter.transient :as t] [clojure.set :as set])) ;;TODO: @@ -1038,3 +1039,34 @@ (is (= 2 (key e))) (is (= 4 (val e))) )) + +(defspec transient-vector-test + (for-all+ + [v (gen/vector (limit-size 5 gen/int))] + (every? identity + (for [[path transient-path f] + [[s/FIRST t/FIRST! (fnil inc 0)] ;; fnil in case vector is empty + [s/LAST t/LAST! (fnil inc 0)] + [(s/keypath 0) (t/keypath! 0) (fnil inc 0)] + [s/END t/END! #(conj % 1 2 3)]]] + (and (= (s/transform* path f v) + (persistent! (s/transform* transient-path f (transient v)))) + (= (s/select* path v) + (s/select* transient-path (transient v)))))))) + +(defspec transient-map-test + (for-all+ + [m (limit-size 5 (gen/not-empty (gen/map gen/keyword gen/int))) + new-key gen/keyword] + (let [existing-key (first (keys m))] + (every? identity + (for [[path transient-path f] + [[(s/keypath existing-key) (t/keypath! existing-key) inc] + [(s/keypath new-key) (t/keypath! new-key) (constantly 3)] + [(s/submap [existing-key new-key]) + (t/submap! [existing-key new-key]) + (constantly {new-key 1234})]]] + (and (= (s/transform* path f m) + (persistent! (s/transform* transient-path f (transient m)))) + (= (s/select* path m) + (s/select* transient-path (transient m)))))))))