specter/src/com/rpl/specter.cljc

326 lines
11 KiB
Text
Raw Normal View History

2015-02-26 15:55:20 +00:00
(ns com.rpl.specter
(:use [com.rpl.specter.protocols :only [StructurePath]])
2015-06-24 18:28:33 +00:00
(:require [com.rpl.specter.impl :as i])
2015-02-26 15:55:20 +00:00
)
;;TODO: can make usage of vals much more efficient by determining during composition how many vals
;;there are going to be. this should make it much easier to allocate space for vals without doing concats
;;all over the place. The apply to the vals + structure can also be avoided since the number of vals is known
;;beforehand
(defn comp-paths [& paths]
(i/comp-paths* (vec paths)))
2015-02-26 15:55:20 +00:00
;; Selector functions
2015-06-18 04:56:03 +00:00
(def ^{:doc "Version of select that takes in a selector pre-compiled with comp-paths"}
2015-06-24 18:28:33 +00:00
compiled-select i/compiled-select*)
(defn select
"Navigates to and returns a sequence of all the elements specified by the selector."
[selector structure]
(compiled-select (i/comp-paths* selector)
structure))
(defn compiled-select-one
"Version of select-one that takes in a selector pre-compiled with comp-paths"
2015-02-26 15:55:20 +00:00
[selector structure]
(let [res (compiled-select selector structure)]
2015-02-26 15:55:20 +00:00
(when (> (count res) 1)
2015-06-24 18:28:33 +00:00
(i/throw-illegal "More than one element found for params: " selector structure))
2015-02-26 15:55:20 +00:00
(first res)
))
(defn select-one
"Like select, but returns either one element or nil. Throws exception if multiple elements found"
[selector structure]
(compiled-select-one (i/comp-paths* selector) structure))
(defn compiled-select-one!
"Version of select-one! that takes in a selector pre-compiled with comp-paths"
2015-02-26 15:55:20 +00:00
[selector structure]
(let [res (compiled-select selector structure)]
2015-06-30 18:31:07 +00:00
(when (not= 1 (count res)) (i/throw-illegal "Expected exactly one element for params: " selector structure))
(first res)
2015-02-26 15:55:20 +00:00
))
(defn select-one!
"Returns exactly one element, throws exception if zero or multiple elements found"
[selector structure]
(compiled-select-one! (i/comp-paths* selector) structure))
(defn compiled-select-first
"Version of select-first that takes in a selector pre-compiled with comp-paths"
[selector structure]
(first (compiled-select selector structure)))
2015-02-26 15:55:20 +00:00
(defn select-first
2015-04-22 15:46:13 +00:00
"Returns first element found. Not any more efficient than select, just a convenience"
2015-02-26 15:55:20 +00:00
[selector structure]
(compiled-select-first (i/comp-paths* selector) structure))
2015-02-26 15:55:20 +00:00
;; Transformfunctions
2015-02-26 15:55:20 +00:00
2015-06-18 04:56:03 +00:00
(def ^{:doc "Version of transform that takes in a selector pre-compiled with comp-paths"}
2015-06-24 18:28:33 +00:00
compiled-transform i/compiled-transform*)
(defn transform
"Navigates to each value specified by the selector and replaces it by the result of running
the transform-fn on it"
[selector transform-fn structure]
(compiled-transform (i/comp-paths* selector) transform-fn structure))
(defn compiled-setval
"Version of setval that takes in a selector pre-compiled with comp-paths"
[selector val structure]
(compiled-transform selector (fn [_] val) structure))
2015-05-10 12:09:48 +00:00
2015-04-22 15:46:13 +00:00
(defn setval
"Navigates to each value specified by the selector and replaces it by val"
[selector val structure]
(compiled-setval (i/comp-paths* selector) val structure))
2015-02-26 15:55:20 +00:00
(defn compiled-replace-in
"Version of replace-in that takes in a selector pre-compiled with comp-paths"
[selector transform-fn structure & {:keys [merge-fn] :or {merge-fn concat}}]
2015-06-24 18:28:33 +00:00
(let [state (i/mutable-cell nil)]
[(compiled-transform selector
2015-02-26 15:55:20 +00:00
(fn [e]
(let [res (transform-fn e)]
2015-02-26 15:55:20 +00:00
(if res
(let [[ret user-ret] res]
(->> user-ret
2015-06-24 18:28:33 +00:00
(merge-fn (i/get-cell state))
(i/set-cell! state))
2015-02-26 15:55:20 +00:00
ret)
e
)))
structure)
2015-06-24 18:28:33 +00:00
(i/get-cell state)]
2015-02-26 15:55:20 +00:00
))
(defn replace-in
"Similar to transform, except returns a pair of [transformd-structure sequence-of-user-ret].
The transform-fn in this case is expected to return [ret user-ret]. ret is
what's used to transform the data structure, while user-ret will be added to the user-ret sequence
in the final return. replace-in is useful for situations where you need to know the specific values
of what was transformd in the data structure."
[selector transform-fn structure & {:keys [merge-fn] :or {merge-fn concat}}]
(compiled-replace-in (i/comp-paths* selector) transform-fn structure :merge-fn merge-fn))
(def bind-params i/bind-params)
;; paramspath* [bindings num-params-sym [impl1 impl2]]
(defmacro paramspath [params & impls]
(let [num-params (count params)
retrieve-params (->> params
(map-indexed
(fn [i p]
[p `(aget ~i/PARAMS-SYM
(+ ~i/PARAMS-IDX-SYM ~i))]
))
(apply concat))]
(i/paramspath* retrieve-params num-params impls)
))
(defmacro defparamspath [name & body]
`(def ~name (paramspath ~@body)))
(defmacro params-paramspath [bindings & impls]
(let [bindings (partition 2 bindings)
paths (mapv second bindings)
names (mapv first bindings)
latefns-sym (gensym "latefns")
latefn-syms (vec (i/gensyms (count paths)))]
(i/params-paramspath*
paths
latefns-sym
[latefn-syms latefns-sym]
(mapcat (fn [n l] [n `(~l ~i/PARAMS-SYM ~i/PARAMS-IDX-SYM)]) names latefn-syms)
impls)))
(defmacro params-varparamspath [[latepaths-seq-sym paths-seq] & impls]
(let [latefns-sym (gensym "latefns")]
(i/params-paramspath*
paths-seq
latefns-sym
[]
[latepaths-seq-sym `(map (fn [l#] (l# ~i/PARAMS-SYM ~i/PARAMS-IDX-SYM))
~latefns-sym)]
impls
)))
2015-02-26 15:55:20 +00:00
;; Built-in pathing and context operations
2015-06-24 18:28:33 +00:00
(def ALL (i/->AllStructurePath))
2015-02-26 15:55:20 +00:00
2015-06-24 18:28:33 +00:00
(def VAL (i/->ValCollect))
2015-02-26 15:55:20 +00:00
2015-06-30 21:07:44 +00:00
(def LAST (i/->PosStructurePath last i/set-last))
2015-02-26 15:55:20 +00:00
2015-06-30 21:07:44 +00:00
(def FIRST (i/->PosStructurePath first i/set-first))
2015-02-26 15:55:20 +00:00
;;TODO: should be parameterized
2015-06-24 18:28:33 +00:00
(defn srange-dynamic [start-fn end-fn] (i/->SRangePath start-fn end-fn))
;;TODO: should be parameterized
(defn srange [start end] (srange-dynamic (fn [_] start) (fn [_] end)))
2015-06-24 18:28:33 +00:00
(def BEGINNING (srange 0 0))
(def END (srange-dynamic count count))
2015-06-24 18:28:33 +00:00
(defn walker [afn] (i/->WalkerStructurePath afn))
2015-02-26 15:55:20 +00:00
2015-06-24 18:28:33 +00:00
(defn codewalker [afn] (i/->CodeWalkerStructurePath afn))
2015-02-26 15:55:20 +00:00
(defn filterer [& path]
(params-paramspath [late path]
(select* [this structure next-fn]
(->> structure (filter #(i/selected?* late %)) doall next-fn))
(transform* [this structure next-fn]
(let [[filtered ancestry] (i/filter+ancestry late structure)
;; the vec is necessary so that we can get by index later
;; (can't get by index for cons'd lists)
next (vec (next-fn filtered))]
(reduce (fn [curr [newi oldi]]
(assoc curr oldi (get next newi)))
(vec structure)
ancestry))
)))
2015-02-26 15:55:20 +00:00
(defparamspath 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)))
))
2015-02-26 15:55:20 +00:00
2015-06-24 18:28:33 +00:00
(defn view [afn] (i/->ViewPath afn))
2015-04-18 16:16:51 +00:00
(defn selected?
"Filters the current value based on whether a selector finds anything.
e.g. (selected? :vals ALL even?) keeps the current element only if an
even number exists for the :vals key"
[& path]
(params-paramspath [late path]
(select* [this structure next-fn]
(i/filter-select
#(i/selected?* late %)
structure
next-fn))
(transform* [this structure next-fn]
(i/filter-transform
#(i/selected?* late %)
structure
next-fn))))
(defn not-selected? [& path]
(params-paramspath [late path]
(select* [this structure next-fn]
(i/filter-select
#(i/not-selected?* late %)
structure
next-fn))
(transform* [this structure next-fn]
(i/filter-transform
#(i/not-selected?* late %)
structure
next-fn))))
(defn transformed
"Navigates to a view of the current value by transforming it with the
specified selector and update-fn."
[path update-fn]
(params-paramspath [late path]
(select* [this structure next-fn]
(next-fn (compiled-transform late update-fn structure)))
(transform* [this structure next-fn]
(next-fn (compiled-transform late update-fn structure)))))
2015-06-24 18:28:33 +00:00
(extend-type #?(:clj clojure.lang.Keyword :cljs cljs.core/Keyword)
2015-02-26 15:55:20 +00:00
StructurePath
2015-05-11 18:02:08 +00:00
(select* [kw structure next-fn]
(next-fn (get structure kw)))
(transform* [kw structure next-fn]
2015-05-11 18:02:08 +00:00
(assoc structure kw (next-fn (get structure kw)))
2015-02-26 15:55:20 +00:00
))
(extend-type #?(:clj clojure.lang.AFn :cljs function)
2015-02-26 15:55:20 +00:00
StructurePath
(select* [afn structure next-fn]
2015-06-30 18:31:07 +00:00
(i/filter-select afn structure next-fn))
(transform* [afn structure next-fn]
2015-06-30 18:31:07 +00:00
(i/filter-transform afn structure next-fn)))
2015-06-30 18:31:07 +00:00
(extend-type #?(:clj clojure.lang.PersistentHashSet :cljs cljs.core/PersistentHashSet)
StructurePath
(select* [aset structure next-fn]
2015-06-30 18:31:07 +00:00
(i/filter-select aset structure next-fn))
(transform* [aset structure next-fn]
2015-06-30 18:31:07 +00:00
(i/filter-transform aset structure next-fn)))
2015-02-26 15:55:20 +00:00
(defn collect [& selector]
(i/->SelectCollector select (i/comp-paths* selector)))
2015-02-26 15:55:20 +00:00
(defn collect-one [& selector]
(i/->SelectCollector select-one (i/comp-paths* selector)))
(defn putval
"Adds an external value to the collected vals. Useful when additional arguments
are required to the transform function that would otherwise require partial
application or a wrapper function.
e.g., incrementing val at path [:a :b] by 3:
(transform [:a :b (putval 3)] + some-map)"
[val]
2015-06-24 18:28:33 +00:00
(i/->PutValCollector val))
2015-06-18 04:56:03 +00:00
;;TODO: test nothing matches case
2015-06-18 04:56:03 +00:00
(defn cond-path
2015-06-19 18:27:22 +00:00
"Takes in alternating cond-path selector cond-path selector...
Tests the structure if selecting with cond-path returns anything.
If so, it uses the following selector for this portion of the navigation.
Otherwise, it tries the next cond-path. If nothing matches, then the structure
is not selected."
2015-06-18 04:56:03 +00:00
[& conds]
(params-varparamspath [compiled-paths conds]
(select* [this structure next-fn]
(if-let [selector (i/retrieve-cond-selector compiled-paths structure)]
(->> (compiled-select selector structure)
(mapcat next-fn)
doall)))
(transform* [this structure next-fn]
(if-let [selector (i/retrieve-cond-selector compiled-paths structure)]
(compiled-transform selector next-fn structure)
structure
))))
2015-06-18 04:56:03 +00:00
(defn if-path
"Like cond-path, but with if semantics."
([cond-p if-path] (cond-path cond-p if-path))
([cond-p if-path else-path]
(cond-path cond-p if-path nil else-path)))
2015-06-25 20:30:27 +00:00
(defn multi-path
"A path that branches on multiple paths. For updates,
applies updates to the paths in order."
[& paths]
(params-varparamspath [compiled-paths paths]
(select* [this structure next-fn]
(->> compiled-paths
(mapcat #(compiled-select % structure))
(mapcat next-fn)
doall
))
(transform* [this structure next-fn]
(reduce
(fn [structure selector]
(compiled-transform selector next-fn structure))
structure
compiled-paths
))))