├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGES.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── LICENSE ├── README.md ├── VERSION ├── bb.edn ├── project.clj ├── scripts ├── benchmarks.clj ├── cljs-repl.sh ├── repl.clj └── run-benchmarks ├── src ├── clj │ └── com │ │ └── rpl │ │ ├── specter.cljc │ │ └── specter │ │ ├── impl.cljc │ │ ├── macros.clj │ │ ├── navs.cljc │ │ ├── protocols.cljc │ │ ├── transients.cljc │ │ ├── util_macros.clj │ │ └── zipper.cljc └── java │ └── com │ └── rpl │ └── specter │ ├── MutableCell.java │ └── Util.java └── test └── com └── rpl └── specter ├── cljs_test_helpers.clj ├── cljs_test_runner.cljs ├── core_test.cljc ├── test_helpers.clj └── zipper_test.cljc /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch_depth: 0 13 | 14 | - name: Setup Babashka 15 | uses: turtlequeue/setup-babashka@v1.3.0 16 | with: 17 | babashka-version: 0.7.8 18 | 19 | - name: Prepare java 20 | uses: actions/setup-java@v2 21 | with: 22 | distribution: 'zulu' 23 | java-version: '8' 24 | 25 | - name: Install clojure tools 26 | uses: DeLaGuardo/setup-clojure@4.0 27 | with: 28 | lein: 2.9.1 29 | 30 | - name: Run clj tests 31 | run: bb test:clj 32 | 33 | - name: Run cljs tests 34 | run: bb test:cljs 35 | 36 | - name: Run babashka tests 37 | run: bb test:bb 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *jar 4 | /lib/ 5 | /classes/ 6 | /target/ 7 | /checkouts/ 8 | .lein-deps-sum 9 | .lein-repl-history 10 | .lein-plugins/ 11 | .lein-failures 12 | .cljs_node_repl 13 | out/ 14 | .cpcache 15 | .cache 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 1.1.4 2 | 3 | * Add arglist metadata to navs (thanks @phronmophobic) 4 | * Improve before-index performance by 150x on lists and 5x on vectors (thanks @jeff303) 5 | * Bug fix: BEFORE-ELEM, AFTER-ELEM, FIRST, LAST, BEGINNING, and END on subvecs now produce vector type in cljs 6 | * Add compatibility with [babashka](https://babashka.org/) 7 | 8 | ## 1.1.3 - 2019-10-13 9 | 10 | * Better AOT behavior: path functions for inline caching and protpath extensions no longer write eval'd class files. You can force path functions to not override `*compile-files*` by binding `com.rpl.specter.impl/*path-compile-files*` to `true`. 11 | * Bug fix: fix throw-illegal in cljs 12 | 13 | ## 1.1.2 - 2018-11-01 14 | 15 | * Eliminate reflection warning 16 | * Bug fix: Fix inline compiler symbol handling so class references can be used as constants within paths 17 | * Bug fix: Fix handling of subvector paths in cljs 18 | 19 | ## 1.1.1 - 2018-04-23 20 | 21 | * ClojureScript 1.10 introduced a change causing the `walker` navigator to fail to walk records. `ALL` has been updated to operate over `MapEntry` in ClojureScript, fixing the issue. 22 | * Change ns form to comply with cljs.core.specs.alpha (thanks @gnl) 23 | * Remove usage of pprint in cljs so advanced optimization doesn't include it 24 | 25 | ## 1.1.0 - 2018-01-02 26 | 27 | * Add `vtransform` variant of `transform` that takes in collected values as a vector in the first argument rather than spliced into argument list. 28 | * Add `vterminal` that takes in collected vals as vector in first argument rather than spliced into argument list. 29 | * Add `compact` navigator. After each step of navigation of its subpath, `compact` removes the collection if it's empty. 30 | * Change `terminal` to be a no-op on select codepath 31 | * Bug fix: `subselect`/`filterer` removes first matched element instead of setting to nil when transformed to empty sequence 32 | 33 | ## 1.0.5 - 2017-11-16 34 | 35 | * Add `regex-nav` navigator for regexes, which navigates to every match in a string and supports replacement with a new substring (thanks @mwfogleman) 36 | * Regexes implicitly convert to `regex-nav` in paths 37 | * Strings, numbers, booleans, characters, and symbols implicitly convert to `keypath` in paths 38 | 39 | ## 1.0.4 - 2017-10-17 40 | 41 | * Add `indexed-vals` navigator, a variant of `INDEXED-VALS` that allows for a customized start index. 42 | * Bug fix: Fix `INDEXED-VALS` invalidly overwriting elements in some transforms involving multiple index changes 43 | 44 | ## 1.0.3 - 2017-08-14 45 | 46 | * Added `before-index` navigator for inserting a single element into a sequence. 47 | * Added `index-nav` navigator for moving an element in a sequence to a new index, shifting other elements in the process. 48 | * Added `INDEXED-VALS` navigator for navigating to every element of a sequence as [index elem] pair. Transform on index portion works the same as `index-nav`. 49 | * Workaround for ClojureScript regression that causes warnings for record fields named "var" or other reserved names 50 | 51 | ## 1.0.2 - 2017-06-12 52 | 53 | * Added `pred=`, `pred<`, `pred>`, `pred<=`, `pred>=` for filtering using common comparisons 54 | * Add `map-key` navigator 55 | * Add `set-elem` navigator 56 | * Add `ALL-WITH-META` navigator 57 | * `walker` and `codewalker` can now be used with `NONE` to remove elements 58 | * Improve `walker` performance by 70% by replacing clojure.walk implementation with custom recursive path 59 | * Extend `ALL` to work on records (navigate to key/value pairs) 60 | * Add ability to declare a function for end index of `srange-dynamic` that takes in the result of the start index fn. Use `end-fn` macro to declare this function (takes in 2 args of [collection, start-index]). Functions defined with normal mechanisms (e.g. `fn`) will still only take in the collection as an argument. 61 | * Workaround for ClojureScript bug that emits warnings for vars named the same as a private var in cljs.core (in this case `NONE`, added as private var to cljs.core with 1.9.562) 62 | * For ALL transforms on maps, interpret transformed key/value pair of size < 2 as removal 63 | * Bug fix: Fix incorrect inline compilation when a dynamic function invocation is nested in a data structure within a parameter to a navigator builder 64 | 65 | ## 1.0.1 - 2017-04-17 66 | 67 | * `subselect`/`filterer` can remove entries in source by transforming to a smaller sequence 68 | * Add `satisfies-protpath?` 69 | * Inline cache vars are marked private so as not to interfere with tooling 70 | * Improve performance of `ALL` transform on lists by 20% 71 | * Bug fix: Using `pred` no longer inserts unnecessary `coerce-nav` call at callsite 72 | * Bug fix: Dynamic navs in argument position to another nav now properly expanded and compiled 73 | * Bug fix: Dynamic parameters nested inside data structures as arguments are now compiled correctly by inline compiler 74 | 75 | ## 1.0.0 - 2017-03-01 76 | 77 | * Transform to `com.rpl.specter/NONE` to remove elements from data structures. Works with `keypath` (for both sequences and maps), `must`, `nthpath`, `ALL`, `MAP-VALS`, `FIRST`, and `LAST` 78 | * Add `nthpath` navigator 79 | * Add `with-fresh-collected` higher order navigator 80 | * Added `traverse-all` which returns a transducer that traverses over all elements matching the given path. 81 | * `select-first` and `select-any` now avoid traversal beyond the first value matched by the path (like when using `ALL`), so they are faster now for those use cases. 82 | * Add `MAP-KEYS` navigator that's more efficient than `[ALL FIRST]` 83 | * Add `NAME` and `NAMESPACE` navigators 84 | * Extend `srange`, `BEGINNING`, `END` to work on strings. Navigates to a substring. 85 | * Extend `FIRST` and `LAST` to work on strings. Navigates to a character. 86 | * Add `BEFORE-ELEM` and `AFTER-ELEM` for prepending or appending a single element to a sequence 87 | * Add `NONE-ELEM` to efficiently add a single element to a set 88 | * Improved `ALL` performance for PersistentHashSet 89 | * Dynamic navs automatically compile sequence returns if completely static 90 | * Eliminate reflection warnings for clj (thanks @mpenet) 91 | * Bug fix: Collected vals now properly passed to subpaths for `if-path`, `selected?`, and `not-selected?` 92 | * Bug fix: `LAST`, `FIRST`, `BEGINNING`, and `END` properly transform subvector types to a vector type 93 | 94 | ## 0.13.2 - 2016-12-21 95 | 96 | * Bug fix: Fix race condition relating to retrieving path from cache and AOT compilation 97 | * Bug fix: LAST no longer converts lists to vectors 98 | * Bug fix: Workaround issue with aot + uberjar 99 | 100 | ## 0.13.1 - 2016-11-07 101 | 102 | * Remove any? in com.rpl.specter.impl to avoid conflict with Clojure 1.9 103 | * Enhanced dynamic navigators to continue expanding if any other dynamic navs are returned 104 | * Added `eachnav` to turn any 1-argument navigator into a navigator that accepts any number of arguments, navigating by each argument in order 105 | * `keypath` and `must` enhanced to take in multiple arguments for concisely specifying multiple steps 106 | * Added `traversed` 107 | * Bug fix: Fix regression from 0.13.0 where [ALL FIRST] on a PersistentArrayMap that created duplicate keys would create an invalid PersistentArrayMap 108 | * Bug fix: Fix problems with multi-path and if-path in latest versions of ClojureScript 109 | * Bug fix: Inline compiler no longer flattens and changes the type of sequential params 110 | 111 | ## 0.13.0 - 2016-09-06 112 | 113 | * BREAKING CHANGE: `com.rpl.specter.macros` namespace removed and all macros moved into core `com.rpl.specter` namespace 114 | * BREAKING CHANGE: Core protocol `Navigator` changed to `RichNavigator` and functions now have an extra argument. 115 | * BREAKING CHANGE: All navigators must be defined with `defnav` and its variations from `com.rpl.specter`. The core protocols may no longer be extended. Existing types can be turned into navigators with the new `IndirectNav` protocol. 116 | * BREAKING CHANGE: Removed `fixed-pathed-nav` and `variable-pathed-nav` and replaced with much more generic `late-bound-nav`. `late-bound-nav` can have normal values be late-bound parameterized (not just paths). Use `late-path` function to indicate which parameters are paths. If all bindings given to `late-bound-nav` are static, the navigator will be resolved and cached immediately. See `transformed` and `selected?` for examples. 117 | * BREAKING CHANGE: Paths can no longer be compiled without their parameters. Instead, use the `path` macro to handle the parameterization. 118 | * BREAKING CHANGE: Parameterized protocol paths now work differently since paths cannot be specified without their parameters. Instead, use the parameter names from the declaration in the extension to specify where the parameters should go. For example: 119 | ```clojure 120 | (defprotocolpath MyProtPath [a]) 121 | (extend-protocolpath MyProtPath 122 | clojure.lang.PersistentArrayMap 123 | (must a)) 124 | ``` 125 | * BREAKING CHANGE: Removed `defpathedfn` and replaced with much more generic `defdynamicnav`. `defdynamicnav` works similar to a macro and takes as input the parameters seen during inline caching. Use `dynamic-param?` to distinguish which parameters are statically specified and which are dynamic. `defdynamicnav` is typically used in conjunction with `late-bound-nav` – see implementation of `selected?` for an example. 126 | * Inline caching now works with locals, dynamic vars, and special forms used in the nav position. When resolved at runtime, those values will be coerced to a navigator if a vector or implicit nav (e.g. keyword). Can hint with ^:direct-nav metadata to remove this coercion if know for sure those values will be implementations of `RichNavigator` interface. 127 | * Redesigned internals so navigators use interface dispatch rather than storing transform/selection functions as separate fields. 128 | * Added `local-declarepath` to assist in making local recursive or mutually recursive paths. Use with `providepath`. 129 | * Added `recursive-path` to assist in making recursive paths, both parameterized and unparameterized. Example: 130 | ```clojure 131 | (let [tree-walker (recursive-path [] p (if-path vector? [ALL p] STAY))] 132 | (select tree-walker [1 [2 [3 4] 5] [[6]]])) 133 | ``` 134 | * Significantly improved performance of paths that use dynamic parameters. 135 | * Inline factoring now parameterizes navigators immediately when all parameters are constants (rather than factoring it to use late-bound parameterization). This creates leaner, faster code. 136 | * Added `IndirectNav` protocol for turning a value type into a navigator. 137 | * Removed `must-cache-paths!`. No longer relevant since all paths can now be compiled and cached. 138 | * Added `with-inline-debug` macro that prints information about the code being analyzed and produced by the inline compiler / cacher. 139 | * Switched codebase from cljx to cljc 140 | * Improved performance of ALL and MAP-VALS on PersistentArrayMap by about 2x 141 | * `defnav` now generates helper functions for every method. For example, `keypath` now has helpers `keypath-select*` and `keypath-transform*`. These functions take parameters `[key structure next-fn]` 142 | * Bug fix: ALL and MAP-VALS transforms on PersistentArrayMap above the threshold now output PersistentArrayMap instead of PersistentHashMap 143 | 144 | 145 | ## 0.12.0 - 2016-08-05 146 | 147 | * BREAKING CHANGE: Changed semantics of `Navigator` protocol `select*` in order to enable very large performance improvements to `select`, `select-one`, `select-first`, and `select-one!`. Custom navigators will need to be updated to conform to the new required semantics. Codebases that do not use custom navigators do not require any changes. See the docstring on the protocol for the details. 148 | * Added `select-any` operation which selects a single element navigated to by the path. Which element returned is undefined. If no elements are navigated to, returns `com.rpl.specter/NONE`. This is the fastest selection operation. 149 | * Added `selected-any?` operation that returns true if any element is navigated to. 150 | * Added `traverse` operation which returns a reducible object of all the elements navigated to by the path. Very efficient. 151 | * Added `multi-transform` operation which can be used to perform multiple transformations in a single traversal. Much more efficient than doing the 152 | transformations with `transform` one after another when the transformations share a lot of navigation. `multi-transform` is used in conjunction with `terminal` and `terminal-val` – see the docstring for details. 153 | * Huge performance improvements to `select`, `select-one`, `select-first`, and `select-one!` 154 | * Huge performance improvement to `multi-path` 155 | * Added META navigator (thanks @aengelberg) 156 | * Added DISPENSE navigator to drop all collected values for subsequent navigation 157 | * Added `collected?` macro to create a filter function which operates on the collected values. 158 | * Error now thrown if a pathedfn (like filterer) is used without being parameterized 159 | * Performance improvement for ALL and MAP-VALS on small maps for Clojure by leveraging IMapIterable interface 160 | * Added low-level `richnav` macro for creating navigators with full flexibility 161 | * Bug fix: multi-path and if-path now work properly with value collection 162 | * Bug fix: END, BEGINNING, FIRST, LAST, and MAP-VALS now work properly on nil 163 | * Bug fix: ALL and MAP-VALS now maintain the comparator of sorted maps 164 | * Bug fix: Using value collection along with `setval` no longer throws exception 165 | * Bug fix: Fix error when trying to use Specter along with AOT compilation 166 | 167 | ## 0.11.2 - 2016-06-09 168 | 169 | * Renamed com.rpl.specter.transient namespace to com.rpl.specter.transients to eliminate ClojureScript compiler warning about reserved keyword 170 | * Eliminated compiler warnings for ClojureScript version 171 | 172 | ## 0.11.1 - 2016-06-08 173 | 174 | * More efficient inline caching for Clojure version, benchmarks show inline caching within 5% of manually precompiled code for all cases 175 | * Added navigators for transients in com.rpl.specter.transient namespace (thanks @aengelberg) 176 | * Huge performance improvement for ALL transform on maps and vectors 177 | * Significant performance improvements for FIRST/LAST for vectors 178 | * Huge performance improvements for `if-path`, `cond-path`, `selected?`, and `not-selected?`, especially for condition path containing only static functions 179 | * Huge performance improvement for `END` on vectors 180 | * Added specialized MAP-VALS navigator that is twice as fast as using [ALL LAST] 181 | * Dropped support for Clojurescript below v1.7.10 182 | * Added :notpath metadata to signify pathedfn arguments that should be treated as regular arguments during inline factoring. If one of these arguments is not a static var reference or non-collection value, the path will not factor. 183 | * Bug fix: `transformed` transform-fn no longer factors into `pred` when an anonymous function during inline factoring 184 | * Bug fix: Fixed nil->val to not replace the val on `false` 185 | * Bug fix: Eliminate reflection when using primitive parameters in an inline cached path 186 | 187 | ## 0.11.0 - 2016-05-31 188 | 189 | * New `path` macro does intelligent inline caching of the provided path. The path is factored into a static portion and into params which may change on each usage of the path (e.g. local parameters). The static part is factored and compiled on the first run-through, and then re-used for all subsequent invocations. As an example, `[ALL (keypath k)]` is factored into `[ALL keypath]`, which is compiled and cached, and `[k]`, which is provided on each execution. If it is not possible to precompile the path (e.g. [ALL some-local-variable]), nothing is cached and the path will be compiled on each run-through. 190 | * BREAKING CHANGE: all `select/transform/setval/replace-in` functions changed to macros and moved to com.rpl.specter.macros namespace. The new macros now automatically wrap the provided path in `path` to enable inline caching. Expect up to a 100x performance improvement without using explicit precompilation, and to be within 2% to 15% of the performance of explicitly precompiled usage. 191 | * Added `select*/transform*/setval*/replace-in*/etc.` functions that have the same functionality as the old `select/transform/setval/replace-in` functions. 192 | * Added `must-cache-paths!` function to throw an error if it is not possible to factor a path into a static portion and dynamic parameters. 193 | * BREAKING CHANGE: `defpath` renamed to `defnav` 194 | * BREAKING CHANGE: `path` renamed to `nav` 195 | * BREAKING CHANGE: `fixed-pathed-path` and `variable-pathed-path` renamed to `fixed-pathed-nav` and `variabled-pathed-nav` 196 | * Added `must` navigator to navigate to a key if and only if it exists in the structure 197 | * Added `continuous-subseqs` navigator 198 | * Added `ATOM` navigator (thanks @rakeshp) 199 | * Added "navigator constructors" that can be defined via `defnavconstructor`. These allow defining a flexible function to parameterize a defnav, and the function integrates with inline caching for high performance. 200 | 201 | 202 | ## 0.10.0 - 2016-04-26 203 | 204 | * Make codebase bootstrap cljs compatible 205 | * Remove usage of reducers in cljs version in favor of transducers (thanks @StephenRudolph) 206 | * ALL now maintains type of queues (thanks @StephenRudolph) 207 | * Added `parser` path (thanks @thomasathorne) 208 | * Added `submap` path (thanks @bfabry) 209 | * Added `subselect` path (thanks @aengelberg) 210 | * Fix filterer to maintain the type of the input sequence in transforms 211 | * Integrated zipper navigation into com.rpl.specter.zipper namespace 212 | 213 | ## 0.9.3 - 2016-04-15 214 | 215 | * Change clojure/clojurescript to provided dependencies 216 | * ALL on maps auto-coerces MapEntry to vector, enabling smoother transformation of map keys 217 | * declarepath can now be parameterized 218 | * Added params-reset which calls its path with the params index walked back by the number of params needed by its path. This enables recursive parameterized paths 219 | * Added convenience syntax for defprotocolpath with no params, e.g. (defprotocolpath foo) 220 | * Rename VOID to STOP 221 | 222 | ## 0.9.2 - 2016-01-26 223 | 224 | * Added VOID selector which navigates nowhere 225 | * Better syntax checking for defpath 226 | * Fixed bug in protocol paths (#48) 227 | * Protocol paths now error when extension has invalid number of needed parameters 228 | * Fix replace-in to work with value collection 229 | * Added STAY selector 230 | * Added stay-then-continue and continue-then-stay selectors which enable pre-order/post-order traversals 231 | * Added declarepath and providepath, which enable arbitrary recursive or mutually recursive paths 232 | * Renamed paramspath to path 233 | 234 | ## 0.9.1 - 2016-01-05 235 | 236 | * Fixed reflection in protocol path code 237 | * Optimized late-bound parameterization for JVM implementation by directly creating the object array rather than use object-array 238 | * Incorrectly specified function names in defpath will now throw error 239 | 240 | ## 0.9.0 - 2015-12-12 241 | 242 | * Fixed bug where comp-paths wouldn't work on lazy seqs in cljs 243 | * Renamed defparamspath and defparamscollector to defpath and defcollector 244 | * For Clojure version only, implemented protocol paths (see #38) 245 | 246 | ## 0.8.0 - 2015-10-10 247 | 248 | * Now compatible with Clojure 1.6.0 and 1.5.1 by switching build to cljx (thanks @MerelyAPseudonym) 249 | * Added subset selector (like srange but for sets) 250 | * Added nil->val, NIL->SET, NIL->LIST, and NIL->VECTOR selectors to make it easier to manipulate maps (e.g. (setval [:akey NIL->VECTOR END] [:a :b] amap) to append that vector into that value for the map, even if nothing was at that value at the start) 251 | 252 | ## 0.7.1 - 2015-09-24 253 | 254 | * view can now be late-bound parameterized 255 | * Added a late-bound parameterized version of using a function as a selector called "pred" 256 | * Added paramsfn helper macro for defining filter functions that take late-bound parameters 257 | * walker and codewalker can now be late-bound parameterized 258 | 259 | ## 0.7.0 - 2015-09-11 260 | 261 | * Added late-bound parameterization feature: allows selectors that require params to be precompiled without the parameters, and the parameters are supplied later in bulk. This effectively enables Specter to be used in any situation with very high performance. 262 | * Converted Specter built-in selectors to use late-bound parameterization when appropriate 263 | * ALL, FIRST, and LAST are now precompiled 264 | 265 | ## 0.6.2 - 2015-07-03 266 | 267 | * Added not-selected? selector 268 | * Added transformed selector 269 | * Sped up CLJS implementation for comp-paths by replacing obj-extends? call with satisfies? 270 | * Fixed CLJS implementation to extend core types appropriately 271 | * Used not-native hint to enable direct method invocation to speed up CLJS implementation 272 | 273 | 274 | ## 0.6.1 - 2015-07-01 275 | 276 | * Huge speedup to ClojureScript implementation by optimizing field access 277 | 278 | ## 0.6.0 - 2015-07-01 279 | 280 | * Added ClojureScript compatibility 281 | 282 | ## 0.5.7 - 2015-06-30 283 | 284 | * Fix bug in select-one! which wouldn't allow nil result 285 | 286 | ## 0.5.6 - 2015-06-29 287 | 288 | * Add multi-path implementation 289 | * change FIRST/LAST to select nothing on an empty sequence 290 | * Allow sets to be used directly as selectors (acts as filter) 291 | 292 | ## 0.5.5 - 2015-06-22 293 | 294 | * Change filterer to accept a selector (that acts like selected? to determine whether or not to select value) 295 | 296 | ## 0.5.4 - 2015-06-19 297 | 298 | * Change cond-path and if-path to take in a selector for conditionals (same idea as selected?) 299 | 300 | ## 0.5.3 - 2015-06-18 301 | 302 | * Added cond-path and if-path selectors for choosing paths depending on value of structure at that location 303 | 304 | ## 0.5.2 - 2015-06-01 305 | 306 | * Fix error for selectors with one element defined using comp-paths, e.g. [:a (comp-paths :b)] 307 | 308 | ## 0.5.1 - 2015-05-31 309 | 310 | * Added putval for adding external values to collected values list 311 | * nil is now interpreted as identity selector 312 | * empty selector is now interpreted as identity selector instead of producing error 313 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome external contributions to this project. For ideas for new functionality, we recommend first opening an issue so we can discuss whether it makes sense for the project. 4 | 5 | ## Contribution process 6 | 7 | 1. Open a pull request. If possible, please include thorough tests of the new code. 8 | 2. If you have not already signed a contributor agreement, we will request that you sign one. We use Adobe Sign for this so it's very quick and easy. Note that we cannot look at your pull request until a contributor agreement is signed. 9 | 3. We will then review your pull request and possibly ask for changes. 10 | -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Running Clojure tests 2 | 3 | ``` 4 | lein do clean, test 5 | ``` 6 | 7 | # Running ClojureScript tests 8 | 9 | ``` 10 | lein javac 11 | lein doo node test-build once 12 | ``` 13 | 14 | # Running benchmarks 15 | ## All benchmarks 16 | ``` 17 | scripts/run-benchmarks 18 | ``` 19 | ## Individual benchmark(s) 20 | Specify the benchmark names as command line args. They will likely each need quoted because they contain spaces. 21 | Order is ignored. 22 | ``` 23 | scripts/run-benchmarks "prepend to a vector" "filter a sequence" 24 | ``` 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Specter 2 | 3 | Specter rejects Clojure's restrictive approach to immutable data structure manipulation, instead exposing an elegant API to allow any sort of manipulation imaginable. Specter especially excels at querying and transforming nested and recursive data, important use cases that are very complex to handle with vanilla Clojure. 4 | 5 | Specter has an extremely simple core, just a single abstraction called "navigator". Queries and transforms are done by composing navigators into a "path" precisely targeting what you want to retrieve or change. Navigators can be composed with any other navigators, allowing sophisticated manipulations to be expressed very concisely. 6 | 7 | In addition, Specter has performance rivaling hand-optimized code (see [this benchmark](https://gist.github.com/nathanmarz/b7c612b417647db80b9eaab618ff8d83)). Clojure's only comparable built-in operations are `get-in` and `update-in`, and the Specter equivalents are 30% and 85% faster respectively (while being just as concise). Under the hood, Specter uses [advanced dynamic techniques](https://github.com/redplanetlabs/specter/wiki/Specter's-inline-caching-implementation) to strip away the overhead of composition. 8 | 9 | There are some key differences between the Clojure approach to data manipulation and the Specter approach. Unlike Clojure, Specter always uses the most efficient method possible to implement an operation for a datatype (e.g. `last` vs. `LAST`). Clojure intentionally leaves out many operations, such as prepending to a vector or inserting into the middle of a sequence. Specter has navigators that cover these use cases (`BEFORE-ELEM` and `before-index`) and many more. Finally, Specter transforms always target precise parts of a data structure, leaving everything else the same. For instance, `ALL` targets every value within a sequence, and the resulting transform is always the same type as the input (e.g. a vector stays a vector, a sorted map stays a sorted map). 10 | 11 | Consider these examples: 12 | 13 | **Example 1: Increment every even number nested within map of vector of maps** 14 | 15 | ```clojure 16 | (def data {:a [{:aa 1 :bb 2} 17 | {:cc 3}] 18 | :b [{:dd 4}]}) 19 | 20 | ;; Manual Clojure 21 | (defn map-vals [m afn] 22 | (->> m (map (fn [[k v]] [k (afn v)])) (into (empty m)))) 23 | 24 | (map-vals data 25 | (fn [v] 26 | (mapv 27 | (fn [m] 28 | (map-vals 29 | m 30 | (fn [v] (if (even? v) (inc v) v)))) 31 | v))) 32 | 33 | ;; Specter 34 | (transform [MAP-VALS ALL MAP-VALS even?] inc data) 35 | ``` 36 | 37 | **Example 2: Append a sequence of elements to a nested vector** 38 | 39 | ```clojure 40 | (def data {:a [1 2 3]}) 41 | 42 | ;; Manual Clojure 43 | (update data :a (fn [v] (into (if v v []) [4 5]))) 44 | 45 | ;; Specter 46 | (setval [:a END] [4 5] data) 47 | ``` 48 | 49 | **Example 3: Increment the last odd number in a sequence** 50 | 51 | ```clojure 52 | (def data [1 2 3 4 5 6 7 8]) 53 | 54 | ;; Manual Clojure 55 | (let [idx (reduce-kv (fn [res i v] (if (odd? v) i res)) nil data)] 56 | (if idx (update data idx inc) data)) 57 | 58 | ;; Specter 59 | (transform [(filterer odd?) LAST] inc data) 60 | ``` 61 | 62 | **Example 4: Map a function over a sequence without changing the type or order of the sequence** 63 | 64 | ```clojure 65 | ;; Manual Clojure 66 | (map inc data) ;; doesn't work, becomes a lazy sequence 67 | (into (empty data) (map inc data)) ;; doesn't work, reverses the order of lists 68 | 69 | ;; Specter 70 | (transform ALL inc data) ;; works for all Clojure datatypes with near-optimal efficiency 71 | ``` 72 | 73 | 74 | 75 | # Latest Version 76 | 77 | The latest release version of Specter is hosted on [Clojars](https://clojars.org): 78 | 79 | [![Current Version](https://clojars.org/com.rpl/specter/latest-version.svg)](https://clojars.org/com.rpl/specter) 80 | 81 | # Learn Specter 82 | 83 | - Introductory blog post: [Clojure's missing piece](http://nathanmarz.com/blog/clojures-missing-piece.html) 84 | - Presentation about Specter: [Specter: Powerful and Simple Data Structure Manipulation](https://www.youtube.com/watch?v=VTCy_DkAJGk) 85 | - Note that this presentation was given before Specter's inline compilation/caching system was developed. You no longer need to do anything special to get near-optimal performance. 86 | - Screencast on Specter: [Understanding Specter](https://www.youtube.com/watch?v=rh5J4vacG98) 87 | - List of navigators with examples: [This wiki page](https://github.com/redplanetlabs/specter/wiki/List-of-Navigators) provides a more comprehensive overview than the API docs about the behavior of specific navigators and includes many examples. 88 | - Core operations and defining new navigators: [This wiki page](https://github.com/redplanetlabs/specter/wiki/List-of-Macros) provides a more comprehensive overview than the API docs of the core select/transform/etc. operations and the operations for defining new navigators. 89 | - [This wiki page](https://github.com/redplanetlabs/specter/wiki/Using-Specter-Recursively) explains how to do precise and efficient recursive navigation with Specter. 90 | - [This wiki page](https://github.com/redplanetlabs/specter/wiki/Using-Specter-With-Zippers) provides a comprehensive overview of how to use Specter's zipper navigators. Zippers are a much slower navigation method but can perform certain tasks that are not possible with Specter's regular navigators. Note that zippers are rarely needed. 91 | - [Cheat Sheet](https://github.com/redplanetlabs/specter/wiki/Cheat-Sheet) 92 | - [API docs](http://redplanetlabs.github.io/specter/) 93 | - Performance guide: [This post](https://github.com/redplanetlabs/specter/wiki/Specter's-inline-caching-implementation) provides an overview of how Specter achieves its performance. 94 | 95 | Specter's API is contained in these files: 96 | 97 | - [specter.cljc](https://github.com/redplanetlabs/specter/blob/master/src/clj/com/rpl/specter.cljc): This contains the built-in navigators and the definition of the core operations. 98 | - [transients.cljc](https://github.com/redplanetlabs/specter/blob/master/src/clj/com/rpl/specter/transients.cljc): This contains navigators for transient collections. 99 | - [zipper.cljc](https://github.com/redplanetlabs/specter/blob/master/src/clj/com/rpl/specter/zipper.cljc): This integrates zipper-based navigation into Specter. 100 | 101 | # Questions? 102 | 103 | You can ask questions about Specter by [opening an issue](https://github.com/redplanetlabs/specter/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aquestion+) on Github. 104 | 105 | You can also find help in the #specter channel on [Clojurians](http://clojurians.net/). 106 | 107 | # Examples 108 | 109 | Increment all the values in maps of maps: 110 | ```clojure 111 | user> (use 'com.rpl.specter) 112 | user> (transform [MAP-VALS MAP-VALS] 113 | inc 114 | {:a {:aa 1} :b {:ba -1 :bb 2}}) 115 | {:a {:aa 2}, :b {:ba 0, :bb 3}} 116 | ``` 117 | 118 | Increment all the even values for :a keys in a sequence of maps: 119 | 120 | ```clojure 121 | user> (transform [ALL :a even?] 122 | inc 123 | [{:a 1} {:a 2} {:a 4} {:a 3}]) 124 | [{:a 1} {:a 3} {:a 5} {:a 3}] 125 | ``` 126 | 127 | Retrieve every number divisible by 3 out of a sequence of sequences: 128 | ```clojure 129 | user> (select [ALL ALL #(= 0 (mod % 3))] 130 | [[1 2 3 4] [] [5 3 2 18] [2 4 6] [12]]) 131 | [3 3 18 6 12] 132 | ``` 133 | 134 | Increment the last odd number in a sequence: 135 | 136 | ```clojure 137 | user> (transform [(filterer odd?) LAST] 138 | inc 139 | [2 1 3 6 9 4 8]) 140 | [2 1 3 6 10 4 8] 141 | ``` 142 | 143 | Remove nils from a nested sequence: 144 | 145 | ```clojure 146 | user> (setval [:a ALL nil?] NONE {:a [1 2 nil 3 nil]}) 147 | {:a [1 2 3]} 148 | ``` 149 | 150 | Remove key/value pair from nested map: 151 | 152 | ```clojure 153 | user> (setval [:a :b :c] NONE {:a {:b {:c 1}}}) 154 | {:a {:b {}}} 155 | ``` 156 | 157 | Remove key/value pair from nested map, removing maps that become empty along the way: 158 | 159 | ```clojure 160 | user> (setval [:a (compact :b :c)] NONE {:a {:b {:c 1}}}) 161 | {} 162 | ``` 163 | 164 | Increment all the odd numbers between indices 1 (inclusive) and 4 (exclusive): 165 | 166 | ```clojure 167 | user> (transform [(srange 1 4) ALL odd?] inc [0 1 2 3 4 5 6 7]) 168 | [0 2 2 4 4 5 6 7] 169 | ``` 170 | 171 | Replace the subsequence from indices 2 to 4 with [:a :b :c :d :e]: 172 | 173 | ```clojure 174 | user> (setval (srange 2 4) [:a :b :c :d :e] [0 1 2 3 4 5 6 7 8 9]) 175 | [0 1 :a :b :c :d :e 4 5 6 7 8 9] 176 | ``` 177 | 178 | Concatenate the sequence [:a :b] to every nested sequence of a sequence: 179 | 180 | ```clojure 181 | user> (setval [ALL END] [:a :b] [[1] '(1 2) [:c]]) 182 | [[1 :a :b] (1 2 :a :b) [:c :a :b]] 183 | ``` 184 | 185 | Get all the numbers out of a data structure, no matter how they're nested: 186 | 187 | ```clojure 188 | user> (select (walker number?) 189 | {2 [1 2 [6 7]] :a 4 :c {:a 1 :d [2 nil]}}) 190 | [2 1 2 1 2 6 7 4] 191 | ``` 192 | 193 | Navigate with string keys: 194 | 195 | ```clojure 196 | user> (select ["a" "b"] 197 | {"a" {"b" 10}}) 198 | [10] 199 | ``` 200 | 201 | Reverse the positions of all even numbers between indices 4 and 11: 202 | 203 | ```clojure 204 | user> (transform [(srange 4 11) (filterer even?)] 205 | reverse 206 | [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]) 207 | [0 1 2 3 10 5 8 7 6 9 4 11 12 13 14 15] 208 | ``` 209 | 210 | Append [:c :d] to every subsequence that has at least two even numbers: 211 | ```clojure 212 | user> (setval [ALL 213 | (selected? (filterer even?) (view count) (pred>= 2)) 214 | END] 215 | [:c :d] 216 | [[1 2 3 4 5 6] [7 0 -1] [8 8] []]) 217 | [[1 2 3 4 5 6 :c :d] [7 0 -1] [8 8 :c :d] []] 218 | ``` 219 | 220 | When doing more involved transformations, you often find you lose context when navigating deep within a data structure and need information "up" the data structure to perform the transformation. Specter solves this problem by allowing you to collect values during navigation to use in the transform function. Here's an example which transforms a sequence of maps by adding the value of the :b key to the value of the :a key, but only if the :a key is even: 221 | 222 | ```clojure 223 | user> (transform [ALL (collect-one :b) :a even?] 224 | + 225 | [{:a 1 :b 3} {:a 2 :b -10} {:a 4 :b 10} {:a 3}]) 226 | [{:b 3, :a 1} {:b -10, :a -8} {:b 10, :a 14} {:a 3}] 227 | ``` 228 | 229 | The transform function receives as arguments all the collected values followed by the navigated to value. So in this case `+` receives the value of the :b key followed by the value of the :a key, and the transform is performed to :a's value. 230 | 231 | The four built-in ways for collecting values are `VAL`, `collect`, `collect-one`, and `putval`. `VAL` just adds whatever element it's currently on to the value list, while `collect` and `collect-one` take in a selector to navigate to the desired value. `collect` works just like `select` by finding a sequence of values, while `collect-one` expects to only navigate to a single value. Finally, `putval` adds an external value into the collected values list. 232 | 233 | 234 | Increment the value for :a key by 10: 235 | ```clojure 236 | user> (transform [:a (putval 10)] 237 | + 238 | {:a 1 :b 3}) 239 | {:b 3 :a 11} 240 | ``` 241 | 242 | 243 | For every map in a sequence, increment every number in :c's value if :a is even or increment :d if :a is odd: 244 | 245 | ```clojure 246 | user> (transform [ALL (if-path [:a even?] [:c ALL] :d)] 247 | inc 248 | [{:a 2 :c [1 2] :d 4} {:a 4 :c [0 10 -1]} {:a -1 :c [1 1 1] :d 1}]) 249 | [{:c [2 3], :d 4, :a 2} {:c [1 11 0], :a 4} {:c [1 1 1], :d 2, :a -1}] 250 | ``` 251 | 252 | "Protocol paths" can be used to navigate on polymorphic data. For example, if you have two ways of storing "account" information: 253 | 254 | ```clojure 255 | (defrecord Account [funds]) 256 | (defrecord User [account]) 257 | (defrecord Family [accounts-list]) 258 | ``` 259 | 260 | You can make an "AccountPath" that dynamically chooses its path based on the type of element it is currently navigated to: 261 | 262 | 263 | ```clojure 264 | (defprotocolpath AccountPath []) 265 | (extend-protocolpath AccountPath 266 | User :account 267 | Family [:accounts-list ALL]) 268 | ``` 269 | 270 | Then, here is how to select all the funds out of a list of `User` and `Family`: 271 | 272 | ```clojure 273 | user> (select [ALL AccountPath :funds] 274 | [(->User (->Account 50)) 275 | (->User (->Account 51)) 276 | (->Family [(->Account 1) 277 | (->Account 2)]) 278 | ]) 279 | [50 51 1 2] 280 | ``` 281 | 282 | The next examples demonstrate recursive navigation. Here's one way to double all the even numbers in a tree: 283 | 284 | ```clojure 285 | (defprotocolpath TreeWalker []) 286 | 287 | (extend-protocolpath TreeWalker 288 | Object nil 289 | clojure.lang.PersistentVector [ALL TreeWalker]) 290 | 291 | (transform [TreeWalker number? even?] #(* 2 %) [:a 1 [2 [[[3]]] :e] [4 5 [6 7]]]) 292 | ;; => [:a 1 [4 [[[3]]] :e] [8 5 [12 7]]] 293 | ``` 294 | 295 | Here's how to reverse the positions of all even numbers in a tree (with order based on a depth first search). This example uses conditional navigation instead of protocol paths to do the walk: 296 | 297 | ```clojure 298 | (def TreeValues 299 | (recursive-path [] p 300 | (if-path vector? 301 | [ALL p] 302 | STAY 303 | ))) 304 | 305 | 306 | (transform (subselect TreeValues even?) 307 | reverse 308 | [1 2 [3 [[4]] 5] [6 [7 8] 9 [[10]]]] 309 | ) 310 | ;; => [1 10 [3 [[8]] 5] [6 [7 4] 9 [[2]]]] 311 | ``` 312 | 313 | # ClojureScript 314 | 315 | Specter supports ClojureScript! However, some of the differences between Clojure and ClojureScript affect how you use Specter in ClojureScript, in particular with the namespace declarations. In Clojure, you might `(use 'com.rpl.specter)` or say `(:require [com.rpl.specter :refer :all])` in your namespace declaration. But in ClojureScript, these options [aren't allowed](https://groups.google.com/d/msg/clojurescript/SzYK08Oduxo/MxLUjg50gQwJ). Instead, consider using one of these options: 316 | 317 | ```clojure 318 | (:require [com.rpl.specter :as s]) 319 | (:require [com.rpl.specter :as s :refer-macros [select transform]]) ;; add in the Specter macros that you need 320 | ``` 321 | 322 | # Future work 323 | - Integrate Specter with other kinds of data structures, such as graphs. Desired navigations include: reduction in topological order, navigate to outgoing/incoming nodes, to a subgraph (with metadata indicating how to attach external edges on transformation), to node attributes, to node values, to specific nodes. 324 | 325 | # clj-kondo 326 | 327 | When using Specter in a project with [clj-kondo](https://github.com/clj-kondo/clj-kondo), a lot of the vars will be considered unresolved because internally Specter defines them with macros. The following configuration snippet will resolve these issues if you include it in your `.clj-kondo/config.edn` file. 328 | 329 | ```clojure 330 | {:lint-as {com.rpl.specter/defcollector clojure.core/defn 331 | com.rpl.specter/defdynamicnav clojure.core/defn 332 | com.rpl.specter/defmacroalias clojure.core/def 333 | com.rpl.specter/defnav clojure.core/defn 334 | com.rpl.specter/defrichnav clojure.core/defn}} 335 | ``` 336 | 337 | # Babashka 338 | 339 | This library is compatible with [babashka](https://babashka.org/) as of specter 1.1.4 and babashka 0.7.8. 340 | 341 | # License 342 | 343 | Copyright 2015-2020 Red Planet Labs, Inc. Specter is licensed under Apache License v2.0. 344 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.5-SNAPSHOT 2 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/clj"] 2 | :tasks 3 | {test:clj {:doc "Run clj tests with leiningen" 4 | :task (shell "lein test")} 5 | 6 | test:cljs {:doc "Run cljs tests with leiningen" 7 | :task (shell "lein doo node test-build once")} 8 | 9 | test:bb {:doc "Run bb tests" 10 | :extra-paths ["test"] 11 | :extra-deps {org.clojure/test.check {:mvn/version "0.9.0"} 12 | io.github.cognitect-labs/test-runner 13 | {:git/tag "v0.5.0" :git/sha "b3fd0d2"} 14 | org.clojure/tools.namespace {:git/url "https://github.com/babashka/tools.namespace" 15 | :git/sha "3625153ee66dfcec2ba600851b5b2cbdab8fae6c"}} 16 | :requires ([cognitect.test-runner :as tr]) 17 | :task (apply tr/-main 18 | "-d" "test" 19 | *command-line-args*)}}} 20 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (def VERSION (.trim (slurp "VERSION"))) 2 | 3 | (defproject com.rpl/specter VERSION 4 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] ; this prevents JVM from doing optimizations which can remove stack traces from NPE and other exceptions 5 | ;"-agentpath:/Applications/YourKit_Java_Profiler_2015_build_15056.app/Contents/Resources/bin/mac/libyjpagent.jnilib"] 6 | 7 | :source-paths ["src/clj"] 8 | :java-source-paths ["src/java"] 9 | :test-paths ["test", "target/test-classes"] 10 | :auto-clean false 11 | :dependencies [[riddley "0.1.12"]] 12 | :plugins [[lein-codox "0.10.7"] 13 | [lein-doo "0.1.7"]] 14 | :codox {:source-paths ["target/classes" "src/clj"] 15 | :namespaces [com.rpl.specter 16 | com.rpl.specter.zipper 17 | com.rpl.specter.protocols 18 | com.rpl.specter.transients] 19 | :source-uri 20 | {#"target/classes" "https://github.com/nathanmarz/specter/tree/{version}/src/clj/{classpath}x#L{line}" 21 | #".*" "https://github.com/nathanmarz/specter/tree/{version}/src/clj/{classpath}#L{line}"}} 22 | 23 | 24 | :cljsbuild {:builds [{:id "test-build" 25 | :source-paths ["src/clj" "target/classes" "test"] 26 | :compiler {:output-to "out/testable.js" 27 | :main 'com.rpl.specter.cljs-test-runner 28 | :target :nodejs 29 | :optimizations :none}}]} 30 | 31 | :profiles {:dev {:dependencies 32 | [[org.clojure/test.check "0.9.0"] 33 | [org.clojure/clojure "1.9.0"] 34 | [org.clojure/clojurescript "1.10.439"]]} 35 | :bench {:dependencies [[org.clojure/clojure "1.9.0"] 36 | [criterium "0.4.4"]]} 37 | :test {:dependencies [[org.clojure/clojure "1.7.0"]]}} 38 | 39 | :deploy-repositories 40 | [["clojars" {:url "https://repo.clojars.org" 41 | :sign-releases false}]] 42 | 43 | :aliases {"deploy" ["do" "clean," "deploy" "clojars"]}) 44 | -------------------------------------------------------------------------------- /scripts/benchmarks.clj: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.benchmarks 2 | (:use [com.rpl.specter] 3 | [com.rpl.specter.transients]) 4 | (:require [clojure.walk :as walk] 5 | [com.rpl.specter.impl :as i] 6 | [criterium.core :as bench])) 7 | 8 | (defn pretty-float3 [anum] 9 | (format "%.3g" anum)) 10 | 11 | (defn mean [a-fn] 12 | (-> a-fn (bench/benchmark* {}) :mean first (* 1000000))) 13 | 14 | (defn compare-benchmark [afn-map] 15 | (let [results (transform MAP-VALS mean afn-map) 16 | [[_ best-time] & _ :as sorted] (sort-by last results)] 17 | (println "\nMean(us)\tvs best\t\tCode") 18 | (doseq [[k t] sorted] 19 | (println (pretty-float3 t) "\t\t" (pretty-float3 (/ t best-time 1.0)) "\t\t" k)))) 20 | 21 | (defmacro run-benchmark [name & exprs] 22 | (let [only-benchmarks (set (filter some? *command-line-args*)) 23 | all-benchmarks? (empty? only-benchmarks)] 24 | (if (or all-benchmarks? (contains? only-benchmarks name)) 25 | (let [afn-map (->> exprs shuffle (map (fn [e] [`(quote ~e) `(fn [] ~e)])) (into {}))] 26 | `(do 27 | (println "Benchmark:" ~name) 28 | (compare-benchmark ~afn-map) 29 | (println "\n********************************\n")))))) 30 | 31 | (defn specter-dynamic-nested-get [data a b c] 32 | (select-any (keypath a b c) data)) 33 | 34 | 35 | (defn get-k [k] (fn [m next] (next (get m k)))) 36 | 37 | (def get-a-b-c 38 | (reduce 39 | (fn [curr afn] 40 | (fn [structure] 41 | (afn structure curr))) 42 | [identity (get-k :c) (get-k :b) (get-k :a)])) 43 | 44 | (let [data {:a {:b {:c 1}}} 45 | p (comp-paths :a :b :c)] 46 | (run-benchmark "get value in nested map" 47 | (select-any [:a :b :c] data) 48 | (select-any (keypath :a :b :c) data) 49 | (select-one [:a :b :c] data) 50 | (select-first [:a :b :c] data) 51 | (select-one! [:a :b :c] data) 52 | (compiled-select-any p data) 53 | (specter-dynamic-nested-get data :a :b :c) 54 | (get-in data [:a :b :c]) 55 | (get-a-b-c data) 56 | (-> data :a :b :c identity) 57 | (-> data (get :a) (get :b) (get :c)) 58 | (-> data :a :b :c) 59 | (select-any [(keypath :a) (keypath :b) (keypath :c)] data))) 60 | 61 | 62 | (let [data {:a {:b {:c 1}}}] 63 | (run-benchmark "set value in nested map" 64 | (assoc-in data [:a :b :c] 1) 65 | (setval [:a :b :c] 1 data))) 66 | 67 | 68 | ;; because below 1.7 there is no update function 69 | (defn- my-update [m k afn] 70 | (assoc m k (afn (get m k)))) 71 | 72 | (defn manual-transform [m afn] 73 | (my-update m :a 74 | (fn [m2] 75 | (my-update m2 :b 76 | (fn [m3] 77 | (my-update m3 :c afn)))))) 78 | 79 | (let [data {:a {:b {:c 1}}}] 80 | (run-benchmark "update value in nested map" 81 | (update-in data [:a :b :c] inc) 82 | (transform [:a :b :c] inc data) 83 | (manual-transform data inc))) 84 | 85 | 86 | (defn map-vals-map-iterable [^clojure.lang.IMapIterable m afn] 87 | (let [k-it (.keyIterator m) 88 | v-it (.valIterator m)] 89 | (loop [ret {}] 90 | (if (.hasNext k-it) 91 | (let [k (.next k-it) 92 | v (.next v-it)] 93 | (recur (assoc ret k (afn v)))) 94 | 95 | ret)))) 96 | 97 | 98 | (defn map-vals-map-iterable-transient [^clojure.lang.IMapIterable m afn] 99 | (persistent! 100 | (let [k-it (.keyIterator m) 101 | v-it (.valIterator m)] 102 | (loop [ret (transient {})] 103 | (if (.hasNext k-it) 104 | (let [k (.next k-it) 105 | v (.next v-it)] 106 | (recur (assoc! ret k (afn v)))) 107 | 108 | ret))))) 109 | 110 | 111 | (let [data '(1 2 3 4 5)] 112 | (run-benchmark "transform values of a list" 113 | (transform ALL inc data) 114 | (doall (sequence (map inc) data)) 115 | (reverse (into '() (map inc) data)) 116 | )) 117 | 118 | (let [data {:a 1 :b 2 :c 3 :d 4}] 119 | (run-benchmark "transform values of a small map" 120 | (into {} (for [[k v] data] [k (inc v)])) 121 | (reduce-kv (fn [m k v] (assoc m k (inc v))) {} data) 122 | (persistent! (reduce-kv (fn [m k v] (assoc! m k (inc v))) (transient {}) data)) 123 | (reduce-kv (fn [m k v] (assoc m k (inc v))) (empty data) data) 124 | (transform [ALL LAST] inc data) 125 | (transform MAP-VALS inc data) 126 | (zipmap (keys data) (map inc (vals data))) 127 | (into {} (map (fn [e] [(key e) (inc (val e))]) data)) 128 | (into {} (map (fn [e] [(key e) (inc (val e))])) data) 129 | (map-vals-map-iterable data inc) 130 | (map-vals-map-iterable-transient data inc) 131 | )) 132 | 133 | 134 | (let [data (->> (for [i (range 1000)] [i i]) (into {}))] 135 | (run-benchmark "transform values of large map" 136 | (into {} (for [[k v] data] [k (inc v)])) 137 | (reduce-kv (fn [m k v] (assoc m k (inc v))) {} data) 138 | (persistent! (reduce-kv (fn [m k v] (assoc! m k (inc v))) (transient {}) data)) 139 | (persistent! (reduce-kv (fn [m k v] (assoc! m k (inc v))) (transient clojure.lang.PersistentHashMap/EMPTY) data)) 140 | (reduce-kv (fn [m k v] (assoc m k (inc v))) (empty data) data) 141 | (transform [ALL LAST] inc data) 142 | (transform MAP-VALS inc data) 143 | (zipmap (keys data) (map inc (vals data))) 144 | (into {} (map (fn [e] [(key e) (inc (val e))]) data)) 145 | (into {} (map (fn [e] [(key e) (inc (val e))])) data) 146 | (map-vals-map-iterable data inc) 147 | (map-vals-map-iterable-transient data inc))) 148 | 149 | 150 | (let [data [1 2 3 4 5 6 7 8 9 10]] 151 | (run-benchmark "first value of a size 10 vector" 152 | (first data) 153 | (select-any ALL data) 154 | (select-any FIRST data) 155 | (select-first ALL data) 156 | )) 157 | 158 | (let [data [1 2 3 4 5]] 159 | (run-benchmark "map a function over a vector" 160 | (vec (map inc data)) 161 | (mapv inc data) 162 | (transform ALL inc data) 163 | (into [] (map inc) data))) 164 | 165 | 166 | (let [data [1 2 3 4 5 6 7 8 9 10]] 167 | (run-benchmark "filter a sequence" 168 | (doall (filter even? data)) 169 | (filterv even? data) 170 | (select [ALL even?] data) 171 | (select-any (filterer even?) data) 172 | (into [] (filter even?) data))) 173 | 174 | 175 | (let [data [{:a 2 :b 2} {:a 1} {:a 4} {:a 6}] 176 | xf (comp (map :a) (filter even?))] 177 | (run-benchmark "even :a values from sequence of maps" 178 | (select [ALL :a even?] data) 179 | (->> data (mapv :a) (filter even?) doall) 180 | (into [] (comp (map :a) (filter even?)) data) 181 | (into [] xf data))) 182 | 183 | 184 | (let [v (vec (range 1000))] 185 | (run-benchmark "Append to a large vector" 186 | (setval END [1] v) 187 | (setval AFTER-ELEM 1 v) 188 | (reduce conj v [1]) 189 | (conj v 1))) 190 | 191 | (let [data [1 2 3 4 5 6 7 8 9 10]] 192 | (run-benchmark "prepend to a vector" 193 | (vec (cons 0 data)) 194 | (setval BEFORE-ELEM 0 data) 195 | (into [0] data) 196 | )) 197 | 198 | (declarepath TreeValues) 199 | 200 | (providepath TreeValues 201 | (if-path vector? 202 | [ALL TreeValues] 203 | STAY)) 204 | 205 | (defprotocolpath TreeValuesProt) 206 | 207 | (extend-protocolpath TreeValuesProt 208 | clojure.lang.PersistentVector [ALL TreeValuesProt] 209 | Object STAY) 210 | 211 | 212 | (defn tree-value-transform [afn atree] 213 | (if (vector? atree) 214 | (mapv #(tree-value-transform afn %) atree) 215 | (afn atree))) 216 | 217 | 218 | (let [data [1 2 [[3]] [4 6 [7 [8]] 10]]] 219 | (run-benchmark "update every value in a tree (represented with vectors)" 220 | (walk/postwalk (fn [e] (if (and (number? e) (even? e)) (inc e) e)) data) 221 | (transform [(walker number?) even?] inc data) 222 | (transform [TreeValues even?] inc data) 223 | (transform [TreeValuesProt even?] inc data) 224 | (tree-value-transform (fn [e] (if (even? e) (inc e) e)) data))) 225 | 226 | 227 | (let [toappend (range 1000)] 228 | (run-benchmark "transient comparison: building up vectors" 229 | (reduce (fn [v i] (conj v i)) [] toappend) 230 | (reduce (fn [v i] (conj! v i)) (transient []) toappend) 231 | (setval END toappend []) 232 | (setval END! toappend (transient [])))) 233 | 234 | (let [toappend (range 1000)] 235 | (run-benchmark "transient comparison: building up vectors one at a time" 236 | (reduce (fn [v i] (conj v i)) [] toappend) 237 | (reduce (fn [v i] (conj! v i)) (transient []) toappend) 238 | (reduce (fn [v i] (setval END [i] v)) [] toappend) 239 | (reduce (fn [v i] (setval END! [i] v)) (transient []) toappend))) 240 | 241 | 242 | (let [data (vec (range 1000)) 243 | tdata (transient data) 244 | tdata2 (transient data)] 245 | (run-benchmark "transient comparison: assoc'ing in vectors" 246 | (assoc data 600 0) 247 | (assoc! tdata 600 0) 248 | (setval (keypath 600) 0 data) 249 | (setval (keypath! 600) 0 tdata2))) 250 | 251 | (let [data (into {} (for [k (range 1000)] 252 | [k (rand)])) 253 | tdata (transient data) 254 | tdata2 (transient data)] 255 | (run-benchmark "transient comparison: assoc'ing in maps" 256 | (assoc data 600 0) 257 | (assoc! tdata 600 0) 258 | (setval (keypath 600) 0 data) 259 | (setval (keypath! 600) 0 tdata2))) 260 | 261 | (defn modify-submap 262 | [m] 263 | (assoc m 0 1 458 89)) 264 | 265 | (let [data (into {} (for [k (range 1000)] 266 | [k (rand)])) 267 | tdata (transient data)] 268 | (run-benchmark "transient comparison: submap" 269 | (transform (submap [600 700]) modify-submap data) 270 | (transform (submap! [600 700]) modify-submap tdata))) 271 | 272 | (let [data {:x 1} 273 | meta-map {:my :metadata}] 274 | (run-benchmark "set metadata" 275 | (with-meta data meta-map) 276 | (setval META meta-map data))) 277 | 278 | (let [data (with-meta {:x 1} {:my :metadata})] 279 | (run-benchmark "get metadata" 280 | (meta data) 281 | (select-any META data))) 282 | 283 | (let [data (with-meta {:x 1} {:my :metadata})] 284 | (run-benchmark "vary metadata" 285 | (vary-meta data assoc :y 2) 286 | (setval [META :y] 2 data))) 287 | 288 | (let [data (range 1000)] 289 | (run-benchmark "Traverse into a set" 290 | (set data) 291 | (set (select ALL data)) 292 | (into #{} (traverse ALL data)) 293 | (persistent! 294 | (reduce conj! (transient #{}) (traverse ALL data))) 295 | (reduce conj #{} (traverse ALL data)))) 296 | 297 | 298 | (defn mult-10 [v] (* 10 v)) 299 | 300 | (let [data [1 2 3 4 5 6 7 8 9]] 301 | (run-benchmark "multi-transform vs. consecutive transforms, one shared nav" 302 | (->> data (transform [ALL even?] mult-10) (transform [ALL odd?] dec)) 303 | (multi-transform [ALL (multi-path [even? (terminal mult-10)] [odd? (terminal dec)])] data))) 304 | 305 | 306 | (let [data [[1 2 3 4 :a] [5] [6 7 :b 8 9] [10 11 12 13]]] 307 | (run-benchmark "multi-transform vs. consecutive transforms, three shared navs" 308 | (->> data (transform [ALL ALL number? even?] mult-10) (transform [ALL ALL number? odd?] dec)) 309 | (multi-transform [ALL ALL number? (multi-path [even? (terminal mult-10)] [odd? (terminal dec)])] data))) 310 | 311 | (let [data {:a 1 :b 2 :c 3 :d 4}] 312 | (run-benchmark "namespace qualify keys of a small map" 313 | (into {} 314 | (map (fn [[k v]] [(keyword (str *ns*) (name k)) v])) 315 | data) 316 | (reduce-kv (fn [m k v] (assoc m (keyword (str *ns*) (name k)) v)) {} data) 317 | (setval [MAP-KEYS NAMESPACE] (str *ns*) data) 318 | )) 319 | 320 | 321 | (let [data (->> (for [i (range 1000)] [(keyword (str i)) i]) (into {}))] 322 | (run-benchmark "namespace qualify keys of a large map" 323 | (into {} 324 | (map (fn [[k v]] [(keyword (str *ns*) (name k)) v])) 325 | data) 326 | (reduce-kv (fn [m k v] (assoc m (keyword (str *ns*) (name k)) v)) {} data) 327 | (setval [MAP-KEYS NAMESPACE] (str *ns*) data) 328 | )) 329 | 330 | (defnav walker-old [afn] 331 | (select* [this structure next-fn] 332 | (i/walk-select afn next-fn structure)) 333 | (transform* [this structure next-fn] 334 | (i/walk-until afn next-fn structure))) 335 | 336 | (let [data {:a [1 2 {:c '(3 4) :d {:e [1 2 3] 7 8 9 10}}]}] 337 | (run-benchmark "walker vs. clojure.walk version" 338 | (transform (walker number?) inc data) 339 | (transform (walker-old number?) inc data) 340 | )) 341 | 342 | (let [size 1000 343 | middle-idx (/ size 2) 344 | v -1 345 | rng (range size) 346 | data-vec (vec rng) 347 | data-lst (apply list rng)] 348 | (run-benchmark "before-index vs. srange in middle (vector)" 349 | (setval (before-index middle-idx) v data-vec) 350 | (setval (srange middle-idx middle-idx) [v] data-vec)) 351 | (run-benchmark "before-index vs. srange in middle (list)" 352 | (setval (before-index middle-idx) v data-lst) 353 | (setval (srange middle-idx middle-idx) [v] data-lst)) 354 | (run-benchmark "before-index at 0 vs. srange vs. cons (list)" 355 | (setval (before-index 0) v data-lst) 356 | (setval (srange 0 0) [v] data-lst) 357 | (cons v data-lst))) 358 | -------------------------------------------------------------------------------- /scripts/cljs-repl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rlwrap java -cp `lein classpath` clojure.main scripts/repl.clj 4 | 5 | -------------------------------------------------------------------------------- /scripts/repl.clj: -------------------------------------------------------------------------------- 1 | (require 2 | '[cljs.repl :as repl] 3 | '[cljs.repl.node :as node]) 4 | 5 | (repl/repl (node/repl-env)) 6 | -------------------------------------------------------------------------------- /scripts/run-benchmarks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | lein javac 4 | lein version 5 | echo 6 | lein show-profiles bench 7 | echo 8 | java -server -XX:MaxInlineSize=100 -cp "$(lein with-profile bench classpath)" clojure.main scripts/benchmarks.clj "$@" 9 | -------------------------------------------------------------------------------- /src/clj/com/rpl/specter.cljc: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter 2 | #?(:cljs (:require-macros 3 | [com.rpl.specter 4 | :refer 5 | [late-bound-nav 6 | late-bound-richnav 7 | late-bound-collector 8 | defcollector 9 | defnav 10 | defdynamicnav 11 | dynamicnav 12 | richnav 13 | defrichnav 14 | recursive-path 15 | select 16 | transform 17 | setval 18 | select-any]] 19 | 20 | [com.rpl.specter.util-macros :refer 21 | [doseqres]])) 22 | ;; workaround for cljs bug that emits warnings for vars named the same as a 23 | ;; private var in cljs.core (in this case `NONE`, added as private var to 24 | ;; cljs.core with 1.9.562) 25 | #?(:cljs (:refer-clojure :exclude [NONE])) 26 | 27 | (:use [com.rpl.specter.protocols :only [ImplicitNav RichNavigator]] 28 | #?(:clj [com.rpl.specter.util-macros :only [doseqres]])) 29 | (:require [com.rpl.specter.impl :as i] 30 | [com.rpl.specter.navs :as n] 31 | #?(:clj [clojure.walk :as cljwalk]) 32 | #?(:clj [com.rpl.specter.macros :as macros]) 33 | [clojure.set :as set])) 34 | 35 | (defn- static-path? [path] 36 | (if (sequential? path) 37 | (every? static-path? path) 38 | (-> path i/dynamic-param? not) 39 | )) 40 | 41 | (defn wrap-dynamic-nav [f] 42 | (fn [& args] 43 | (let [ret (apply f args)] 44 | (cond (and (sequential? ret) (static-path? ret)) 45 | (i/comp-paths* ret) 46 | 47 | (and (sequential? ret) (= 1 (count ret))) 48 | (first ret) 49 | 50 | :else 51 | ret 52 | )))) 53 | 54 | #?(:clj 55 | (do 56 | 57 | (defmacro defmacroalias [name target] 58 | `(do 59 | (def ~name (var ~target)) 60 | (alter-meta! (var ~name) merge {:macro true}))) 61 | 62 | (defmacroalias richnav macros/richnav) 63 | (defmacroalias nav macros/nav) 64 | (defmacroalias defnav macros/defnav) 65 | (defmacroalias defrichnav macros/defrichnav) 66 | 67 | (defmacro collector [params [_ [_ structure-sym] & body]] 68 | `(richnav ~params 69 | (~'select* [this# vals# ~structure-sym next-fn#] 70 | (next-fn# (conj vals# (do ~@body)) ~structure-sym)) 71 | (~'transform* [this# vals# ~structure-sym next-fn#] 72 | (next-fn# (conj vals# (do ~@body)) ~structure-sym)))) 73 | 74 | (defmacro defcollector [name & body] 75 | `(def ~name (collector ~@body))) 76 | 77 | 78 | (defn- late-bound-operation [bindings builder-op impls] 79 | (let [bindings (partition 2 bindings) 80 | params (map first bindings) 81 | curr-params (map second bindings)] 82 | `(let [builder# (~builder-op [~@params] ~@impls) 83 | curr-params# [~@curr-params]] 84 | (if (every? (complement i/dynamic-param?) curr-params#) 85 | (apply builder# curr-params#) 86 | (com.rpl.specter.impl/->DynamicFunction builder# curr-params# nil))))) 87 | 88 | (defmacro late-bound-nav [bindings & impls] 89 | (late-bound-operation bindings `nav impls)) 90 | 91 | (defmacro late-bound-collector [bindings impl] 92 | (late-bound-operation bindings `collector [impl])) 93 | 94 | (defmacro late-bound-richnav [bindings & impls] 95 | (late-bound-operation bindings `richnav impls)) 96 | 97 | (defmacro with-inline-debug [& body] 98 | `(binding [i/*DEBUG-INLINE-CACHING* true] 99 | ~@body)) 100 | 101 | (defmacro declarepath [name] 102 | `(def ~name (i/local-declarepath))) 103 | 104 | (defmacro providepath [name apath] 105 | `(i/providepath* ~name (path ~apath))) 106 | 107 | (defmacro recursive-path [params self-sym path] 108 | (if (empty? params) 109 | `(let [~self-sym (i/local-declarepath)] 110 | (providepath ~self-sym ~path) 111 | ~self-sym) 112 | `(i/direct-nav-obj 113 | (fn ~params 114 | (let [~self-sym (i/local-declarepath)] 115 | (providepath ~self-sym ~path) 116 | ~self-sym))))) 117 | 118 | ;; copied from clojure.core 119 | (def 120 | ^{:private true} 121 | sigs 122 | (fn [fdecl] 123 | (let [asig 124 | (fn [fdecl] 125 | (let [arglist (first fdecl) 126 | ;; elide implicit macro args 127 | arglist (if (= '&form (first arglist)) 128 | (subvec arglist 2 (count arglist)) 129 | arglist) 130 | body (next fdecl)] 131 | (if (map? (first body)) 132 | (if (next body) 133 | (with-meta arglist (conj (if (meta arglist) (meta arglist) {}) (first body))) 134 | arglist) 135 | arglist)))] 136 | (if (seq? (first fdecl)) 137 | (loop [ret [] fdecls fdecl] 138 | (if fdecls 139 | (recur (conj ret (asig (first fdecls))) (next fdecls)) 140 | (seq ret))) 141 | (list (asig fdecl)))))) 142 | 143 | ;; partially copied from clojure.core/defn 144 | (defn- name-with-attributes 145 | "To be used in macro definitions. 146 | Handles optional docstrings and attribute maps for a name to be defined 147 | in a list of macro arguments. If the first macro argument is a string, 148 | it is added as a docstring to name and removed from the macro argument 149 | list. If afterwards the first macro argument is a map, its entries are 150 | added to the name's metadata map and the map is removed from the 151 | macro argument list. The return value is a vector containing the name 152 | with its extended metadata map and the list of unprocessed macro 153 | arguments." 154 | [name fdecl] 155 | (let [m (if (string? (first fdecl)) 156 | {:doc (first fdecl)} 157 | {}) 158 | 159 | fdecl (if (string? (first fdecl)) 160 | (next fdecl) 161 | fdecl) 162 | m (if (map? (first fdecl)) 163 | (conj m (first fdecl)) 164 | m) 165 | fdecl (if (map? (first fdecl)) 166 | (next fdecl) 167 | fdecl) 168 | fdecl (if (vector? (first fdecl)) 169 | (list fdecl) 170 | fdecl) 171 | m (if (map? (last fdecl)) 172 | (conj m (last fdecl)) 173 | m) 174 | fdecl (if (map? (last fdecl)) 175 | (butlast fdecl) 176 | fdecl) 177 | m (conj {:arglists (list 'quote (sigs fdecl))} m)] 178 | [(with-meta name m) fdecl])) 179 | 180 | (defmacro dynamicnav [& args] 181 | `(vary-meta (wrap-dynamic-nav (fn ~@args)) assoc :dynamicnav true)) 182 | 183 | (defmacro defdynamicnav 184 | "Defines a function that can choose what navigator to use at runtime based on 185 | the dynamic context. The arguments will either be static values or 186 | objects satisfying `dynamic-param?`. Use `late-bound-nav` to produce a runtime 187 | navigator that uses the values of the dynamic params. See `selected?` for 188 | an illustrative example of dynamic navs." 189 | [name & args] 190 | (let [[name args] (name-with-attributes name args)] 191 | `(def ~name (dynamicnav ~@args)))) 192 | 193 | 194 | (defn- ic-prepare-path [locals-set path] 195 | (cond 196 | (vector? path) 197 | (mapv #(ic-prepare-path locals-set %) path) 198 | 199 | (symbol? path) 200 | (if (contains? locals-set path) 201 | (let [s (get locals-set path) 202 | embed (i/maybe-direct-nav path (-> s meta :direct-nav))] 203 | `(com.rpl.specter.impl/->LocalSym ~path (quote ~embed))) 204 | ;; var-get doesn't work in cljs, so capture the val in the macro instead 205 | `(com.rpl.specter.impl/->VarUse 206 | ~path 207 | ~(if-not (instance? Class (resolve path)) `(var ~path)) 208 | (quote ~path))) 209 | 210 | 211 | (i/fn-invocation? path) 212 | (let [[op & params] path] 213 | ;; need special case for 'fn since macroexpand does NOT 214 | ;; expand fn when run on cljs code, but it's also not considered a special symbol 215 | (if (or (= 'fn op) (special-symbol? op)) 216 | `(com.rpl.specter.impl/->SpecialFormUse ~path (quote ~path)) 217 | `(com.rpl.specter.impl/->FnInvocation 218 | ~(ic-prepare-path locals-set op) 219 | ~(mapv #(ic-prepare-path locals-set %) params) 220 | (quote ~path)))) 221 | 222 | 223 | :else 224 | (if (empty? (i/used-locals locals-set path)) 225 | path 226 | `(com.rpl.specter.impl/->DynamicVal (quote ~path))))) 227 | 228 | 229 | (defn- ic-possible-params [path] 230 | (do 231 | (mapcat 232 | (fn [e] 233 | (cond (or (set? e) 234 | (map? e) 235 | (symbol? e) 236 | (and (i/fn-invocation? e) 237 | (or (contains? #{'fn* 'fn} (first e)) 238 | (special-symbol? (first e))))) 239 | [e] 240 | 241 | (sequential? e) 242 | (concat (if (vector? e) [e]) (ic-possible-params e)))) 243 | 244 | 245 | path))) 246 | 247 | 248 | (defn- cljs-macroexpand [env form] 249 | (let [expand-fn (i/cljs-analyzer-macroexpand-1) 250 | mform (expand-fn env form)] 251 | (cond (identical? form mform) mform 252 | (and (seq? mform) (#{'js*} (first mform))) form 253 | :else (cljs-macroexpand env mform)))) 254 | 255 | (defn- cljs-macroexpand-all* [env form] 256 | (if (and (seq? form) 257 | (#{'fn 'fn* 'cljs.core/fn} (first form))) 258 | form 259 | (let [expanded (if (seq? form) (cljs-macroexpand env form) form)] 260 | (cljwalk/walk #(cljs-macroexpand-all* env %) identity expanded)))) 261 | 262 | 263 | (defn- cljs-macroexpand-all [env form] 264 | (let [ret (cljs-macroexpand-all* env form)] 265 | ret)) 266 | 267 | 268 | (defmacro path 269 | "Same as calling comp-paths, except it caches the composition of the static parts 270 | of the path for later re-use (when possible). For almost all idiomatic uses 271 | of Specter provides huge speedup. This macro is automatically used by the 272 | select/transform/setval/replace-in/etc. macros." 273 | [& path] 274 | (let [;;this is a hack, but the composition of &env is considered stable for cljs 275 | platform (if (contains? &env :locals) :cljs :clj) 276 | local-syms (if (= platform :cljs) 277 | (-> &env :locals keys set) ;cljs 278 | (-> &env keys set)) ;clj 279 | 280 | used-locals (i/used-locals local-syms path) 281 | 282 | ;; note: very important to use riddley's macroexpand-all here, so that 283 | ;; &env is preserved in any potential nested calls to select (like via 284 | ;; a view function) 285 | expanded (if (= platform :clj) 286 | (i/clj-macroexpand-all (vec path)) 287 | (cljs-macroexpand-all &env (vec path))) 288 | 289 | prepared-path (ic-prepare-path local-syms expanded) 290 | possible-params (vec (ic-possible-params expanded)) 291 | 292 | cache-sym (vary-meta 293 | (gensym "pathcache") 294 | merge {:cljs.analyzer/no-resolve true :no-doc true :private true}) 295 | 296 | info-sym (gensym "info") 297 | 298 | get-cache-code (if (= platform :clj) 299 | `(try (i/get-cell ~cache-sym) 300 | (catch ClassCastException e# 301 | ;; With AOT compilation it's possible for: 302 | ;; Thread 1: unbound, so throw exception 303 | ;; Thread 2: unbound, so throw exception 304 | ;; Thread 1: do alter-var-root 305 | ;; Thread 2: it's bound, so retrieve the current value 306 | (if (bound? (var ~cache-sym)) 307 | (i/get-cell ~cache-sym) 308 | (do 309 | (alter-var-root 310 | (var ~cache-sym) 311 | (fn [_#] (i/mutable-cell))) 312 | nil)))) 313 | cache-sym) 314 | 315 | add-cache-code (if (= platform :clj) 316 | `(i/set-cell! ~cache-sym ~info-sym) 317 | `(def ~cache-sym ~info-sym)) 318 | 319 | precompiled-sym (gensym "precompiled") 320 | 321 | handle-params-code 322 | (if (= platform :clj) 323 | `(~precompiled-sym ~@used-locals) 324 | `(~precompiled-sym ~possible-params))] 325 | (if (= platform :clj) 326 | (i/intern* *ns* cache-sym (i/mutable-cell))) 327 | `(let [info# ~get-cache-code 328 | 329 | info# 330 | (if (nil? info#) 331 | (let [~info-sym (i/magic-precompilation 332 | ~prepared-path 333 | ~(str *ns*) 334 | (quote ~used-locals) 335 | (quote ~possible-params))] 336 | ~add-cache-code 337 | ~info-sym) 338 | info#) 339 | 340 | ~precompiled-sym (i/cached-path-info-precompiled info#) 341 | dynamic?# (i/cached-path-info-dynamic? info#)] 342 | (if dynamic?# 343 | ~handle-params-code 344 | ~precompiled-sym)))) 345 | 346 | 347 | (defmacro select 348 | "Navigates to and returns a sequence of all the elements specified by the path. 349 | This macro will do inline caching of the path." 350 | [apath structure] 351 | `(i/compiled-select* (path ~apath) ~structure)) 352 | 353 | (defmacro select-one! 354 | "Returns exactly one element, throws exception if zero or multiple elements found. 355 | This macro will do inline caching of the path." 356 | [apath structure] 357 | `(i/compiled-select-one!* (path ~apath) ~structure)) 358 | 359 | (defmacro select-one 360 | "Like select, but returns either one element or nil. Throws exception if multiple elements found. 361 | This macro will do inline caching of the path." 362 | [apath structure] 363 | `(i/compiled-select-one* (path ~apath) ~structure)) 364 | 365 | (defmacro select-first 366 | "Returns first element found. 367 | This macro will do inline caching of the path." 368 | [apath structure] 369 | `(i/compiled-select-first* (path ~apath) ~structure)) 370 | 371 | (defmacro select-any 372 | "Returns any element found or [[NONE]] if nothing selected. This is the most 373 | efficient of the various selection operations. 374 | This macro will do inline caching of the path." 375 | [apath structure] 376 | `(i/compiled-select-any* (path ~apath) ~structure)) 377 | 378 | (defmacro selected-any? 379 | "Returns true if any element was selected, false otherwise. 380 | This macro will do inline caching of the path." 381 | [apath structure] 382 | `(i/compiled-selected-any?* (path ~apath) ~structure)) 383 | 384 | (defmacro transform 385 | "Navigates to each value specified by the path and replaces it by the result of running 386 | the transform-fn on it. 387 | This macro will do inline caching of the path." 388 | [apath transform-fn structure] 389 | `(i/compiled-transform* (path ~apath) ~transform-fn ~structure)) 390 | 391 | (defmacro vtransform 392 | "Navigates to each value specified by the path and replaces it by the result of running 393 | the transform-fn on two arguments: the collected values as a vector, and the navigated value." 394 | [apath transform-fn structure] 395 | `(i/compiled-vtransform* (path ~apath) ~transform-fn ~structure)) 396 | 397 | (defmacro multi-transform 398 | "Just like `transform` but expects transform functions to be specified 399 | inline in the path using `terminal` or `vterminal`. Error is thrown if navigation finishes 400 | at a non-terminal navigator. `terminal-val` is a wrapper around `terminal` and is 401 | the `multi-transform` equivalent of `setval`. 402 | This macro will do inline caching of the path." 403 | [apath structure] 404 | `(i/compiled-multi-transform* (path ~apath) ~structure)) 405 | 406 | 407 | (defmacro setval 408 | "Navigates to each value specified by the path and replaces it by `aval`. 409 | This macro will do inline caching of the path." 410 | [apath aval structure] 411 | `(i/compiled-setval* (path ~apath) ~aval ~structure)) 412 | 413 | (defmacro traverse 414 | "Return a reducible object that traverses over `structure` to every element 415 | specified by the path. 416 | This macro will do inline caching of the path." 417 | [apath structure] 418 | `(i/do-compiled-traverse (path ~apath) ~structure)) 419 | 420 | (defmacro traverse-all 421 | "Returns a transducer that traverses over each element with the given path." 422 | [apath] 423 | `(i/compiled-traverse-all* (path ~apath))) 424 | 425 | (defmacro replace-in 426 | "Similar to transform, except returns a pair of [transformed-structure sequence-of-user-ret]. 427 | The transform-fn in this case is expected to return [ret user-ret]. ret is 428 | what's used to transform the data structure, while user-ret will be added to the user-ret sequence 429 | in the final return. replace-in is useful for situations where you need to know the specific values 430 | of what was transformed in the data structure. 431 | This macro will do inline caching of the path." 432 | [apath transform-fn structure & args] 433 | `(i/compiled-replace-in* (path ~apath) ~transform-fn ~structure ~@args)) 434 | 435 | (defmacro collected? 436 | "Creates a filter function navigator that takes in all the collected values 437 | as input. For arguments, can use `(collected? [a b] ...)` syntax to look 438 | at each collected value as individual arguments, or `(collected? v ...)` syntax 439 | to capture all the collected values as a single vector." 440 | [params & body] 441 | `(i/collected?* (~'fn [~params] ~@body))) 442 | 443 | 444 | (defn- protpath-sym [name] 445 | (-> name (str "-prot") symbol)) 446 | 447 | (defn- protpath-meth-sym [name] 448 | (-> name (str "-retrieve") symbol)) 449 | 450 | 451 | (defmacro defprotocolpath 452 | "Defines a navigator that chooses the path to take based on the type 453 | of the value at the current point. May be specified with parameters to 454 | specify that all extensions must require that number of parameters. 455 | 456 | Currently not available for ClojureScript. 457 | 458 | Example of usage: 459 | (defrecord SingleAccount [funds]) 460 | (defrecord FamilyAccount [single-accounts]) 461 | 462 | (defprotocolpath FundsPath) 463 | (extend-protocolpath FundsPath 464 | SingleAccount :funds 465 | FamilyAccount [ALL FundsPath] 466 | ) 467 | " 468 | ([name] 469 | `(defprotocolpath ~name [])) 470 | ([name params] 471 | (let [prot-name (protpath-sym name) 472 | m (protpath-meth-sym name) 473 | num-params (count params) 474 | ssym (gensym "structure") 475 | rargs [(gensym "vals") ssym (gensym "next-fn")] 476 | retrieve `(~m ~ssym ~@params)] 477 | `(do 478 | (defprotocol ~prot-name (~m [structure# ~@params])) 479 | (defrichnav ~name ~params 480 | (~'select* [this# ~@rargs] 481 | (let [inav# ~retrieve] 482 | (i/exec-select* inav# ~@rargs))) 483 | (~'transform* [this# ~@rargs] 484 | (let [inav# ~retrieve] 485 | (i/exec-transform* inav# ~@rargs)))))))) 486 | 487 | (defmacro satisfies-protpath? [protpath o] 488 | `(satisfies? ~(protpath-sym protpath) ~o)) 489 | 490 | (defn extend-protocolpath* [protpath-prot extensions] 491 | (let [m (-> protpath-prot :sigs keys first) 492 | params (-> protpath-prot :sigs first last :arglists first)] 493 | (doseq [[atype path-code] extensions] 494 | (extend atype protpath-prot 495 | {m (binding [*compile-files* false] 496 | (eval `(fn ~params (path ~path-code))))})))) 497 | 498 | (defmacro extend-protocolpath 499 | "Used in conjunction with `defprotocolpath`. See [[defprotocolpath]]." 500 | [protpath & extensions] 501 | (let [extensions (partition 2 extensions) 502 | embed (vec (for [[t p] extensions] [t `(quote ~p)]))] 503 | `(extend-protocolpath* 504 | ~(protpath-sym protpath) 505 | ~embed))) 506 | 507 | (defmacro end-fn [& args] 508 | `(n/->SrangeEndFunction (fn ~@args))) 509 | 510 | )) 511 | 512 | 513 | 514 | (defn comp-paths 515 | "Returns a compiled version of the given path for use with 516 | compiled-{select/transform/setval/etc.} functions." 517 | [& apath] 518 | (i/comp-paths* (vec apath))) 519 | 520 | ;; Selection functions 521 | 522 | (def ^{:doc "Version of select that takes in a path precompiled with comp-paths"} 523 | compiled-select i/compiled-select*) 524 | 525 | (defn select* 526 | "Navigates to and returns a sequence of all the elements specified by the path." 527 | [path structure] 528 | (compiled-select (i/comp-paths* path) 529 | structure)) 530 | 531 | (def ^{:doc "Version of select-one that takes in a path precompiled with comp-paths"} 532 | compiled-select-one i/compiled-select-one*) 533 | 534 | (defn select-one* 535 | "Like select, but returns either one element or nil. Throws exception if multiple elements found" 536 | [path structure] 537 | (compiled-select-one (i/comp-paths* path) structure)) 538 | 539 | (def ^{:doc "Version of select-one! that takes in a path precompiled with comp-paths"} 540 | compiled-select-one! i/compiled-select-one!*) 541 | 542 | (defn select-one!* 543 | "Returns exactly one element, throws exception if zero or multiple elements found" 544 | [path structure] 545 | (compiled-select-one! (i/comp-paths* path) structure)) 546 | 547 | (def ^{:doc "Version of select-first that takes in a path precompiled with comp-paths"} 548 | compiled-select-first i/compiled-select-first*) 549 | 550 | 551 | (defn select-first* 552 | "Returns first element found." 553 | [path structure] 554 | (compiled-select-first (i/comp-paths* path) structure)) 555 | 556 | (def ^{:doc "Version of select-any that takes in a path precompiled with comp-paths"} 557 | compiled-select-any i/compiled-select-any*) 558 | 559 | (def ^{:doc "Global value used to indicate no elements selected during 560 | [[select-any]]."} 561 | NONE i/NONE) 562 | 563 | (defn select-any* 564 | "Returns any element found or [[NONE]] if nothing selected. This is the most 565 | efficient of the various selection operations." 566 | [path structure] 567 | (compiled-select-any (i/comp-paths* path) structure)) 568 | 569 | (def ^{:doc "Version of selected-any? that takes in a path precompiled with comp-paths"} 570 | compiled-selected-any? i/compiled-selected-any?*) 571 | 572 | (defn selected-any?* 573 | "Returns true if any element was selected, false otherwise." 574 | [path structure] 575 | (compiled-selected-any? (i/comp-paths* path) structure)) 576 | 577 | ;; Reducible traverse functions 578 | 579 | (def ^{:doc "Version of traverse that takes in a path precompiled with comp-paths"} 580 | compiled-traverse i/do-compiled-traverse) 581 | 582 | (defn traverse* 583 | "Return a reducible object that traverses over `structure` to every element 584 | specified by the path" 585 | [apath structure] 586 | (compiled-traverse (i/comp-paths* apath) structure)) 587 | 588 | (def ^{:doc "Version of traverse-all that takes in a path precompiled with comp-paths"} 589 | compiled-traverse-all i/compiled-traverse-all*) 590 | 591 | (defn traverse-all* 592 | "Returns a transducer that traverses over each element with the given path." 593 | [apath] 594 | (compiled-traverse-all (i/comp-paths* apath))) 595 | 596 | ;; Transformation functions 597 | 598 | (def ^{:doc "Version of transform that takes in a path precompiled with comp-paths"} 599 | compiled-transform i/compiled-transform*) 600 | 601 | (def ^{:doc "Version of vtransform that takes in a path precompiled with comp-paths"} 602 | compiled-vtransform i/compiled-vtransform*) 603 | 604 | 605 | (defn transform* 606 | "Navigates to each value specified by the path and replaces it by the result of running 607 | the transform-fn on it" 608 | [path transform-fn structure] 609 | (compiled-transform (i/comp-paths* path) transform-fn structure)) 610 | 611 | (def ^{:doc "Version of `multi-transform` that takes in a path precompiled with `comp-paths`"} 612 | compiled-multi-transform i/compiled-multi-transform*) 613 | 614 | 615 | (defn multi-transform* 616 | "Just like `transform` but expects transform functions to be specified 617 | inline in the path using `terminal` or `vterminal`. Error is thrown if navigation finishes 618 | at a non-terminal navigator. `terminal-val` is a wrapper around `terminal` and is 619 | the `multi-transform` equivalent of `setval`." 620 | [path structure] 621 | (compiled-multi-transform (i/comp-paths* path) structure)) 622 | 623 | 624 | (def ^{:doc "Version of setval that takes in a path precompiled with comp-paths"} 625 | compiled-setval i/compiled-setval*) 626 | 627 | (defn setval* 628 | "Navigates to each value specified by the path and replaces it by val" 629 | [path val structure] 630 | (compiled-setval (i/comp-paths* path) val structure)) 631 | 632 | (def ^{:doc "Version of replace-in that takes in a path precompiled with comp-paths"} 633 | compiled-replace-in i/compiled-replace-in*) 634 | 635 | (defn replace-in* 636 | "Similar to transform, except returns a pair of [transformed-structure sequence-of-user-ret]. 637 | The transform-fn in this case is expected to return [ret user-ret]. ret is 638 | what's used to transform the data structure, while user-ret will be added to the user-ret sequence 639 | in the final return. replace-in is useful for situations where you need to know the specific values 640 | of what was transformed in the data structure." 641 | [path transform-fn structure & {:keys [merge-fn] :or {merge-fn concat}}] 642 | (compiled-replace-in (i/comp-paths* path) transform-fn structure :merge-fn merge-fn)) 643 | 644 | ;; Helper for making late-bound navs 645 | 646 | (def late-path i/late-path) 647 | (def dynamic-param? i/dynamic-param?) 648 | (def late-resolved-fn i/late-resolved-fn) 649 | 650 | 651 | (defdynamicnav 652 | ^{:doc "Turns a navigator that takes one argument into a navigator that takes 653 | many arguments and uses the same navigator with each argument. There 654 | is no performance cost to using this. See implementation of `keypath`"} 655 | eachnav 656 | [navfn] 657 | (let [latenavfn (late-resolved-fn navfn)] 658 | (dynamicnav [& args] 659 | (map latenavfn args)))) 660 | 661 | 662 | ;; Helpers for making recursive or mutually recursive navs 663 | 664 | (def local-declarepath i/local-declarepath) 665 | 666 | ;; Built-in pathing and context operations 667 | 668 | (defnav 669 | ^{:doc "Stops navigation at this point. For selection returns nothing and for 670 | transformation returns the structure unchanged"} 671 | STOP 672 | [] 673 | (select* [this structure next-fn] 674 | NONE) 675 | (transform* [this structure next-fn] 676 | structure)) 677 | 678 | 679 | 680 | (def 681 | ^{:doc "Stays navigated at the current point. Essentially a no-op navigator."} 682 | STAY 683 | i/STAY*) 684 | 685 | (def 686 | ^{:doc "Defines an endpoint in the navigation the transform function run. The transform 687 | function works just like it does in `transform`, with collected values 688 | given as the first arguments"} 689 | terminal 690 | (richnav [afn] 691 | (select* [this vals structure next-fn] 692 | NONE) 693 | (transform* [this vals structure next-fn] 694 | (i/terminal* afn vals structure)))) 695 | 696 | (def 697 | ^{:doc "Defines an endpoint in the navigation the transform function run.The transform 698 | function works differently than it does in `transform`. Rather than receive 699 | collected vals spliced in as the first arguments to the function, this function 700 | always takes two arguemnts. The first is all collected vals in a vector, and 701 | the second is the navigated value."} 702 | vterminal 703 | (richnav [afn] 704 | (select* [this vals structure next-fn] 705 | NONE) 706 | (transform* [this vals structure next-fn] 707 | (afn vals structure)))) 708 | 709 | (defn ^:direct-nav terminal-val 710 | "Like `terminal` but specifies a val to set at the location regardless of 711 | the collected values or the value at the location." 712 | [v] 713 | (terminal (i/fast-constantly v))) 714 | 715 | (defnav 716 | ^{:doc "Navigate to every element of the collection. For maps navigates to 717 | a vector of `[key value]`."} 718 | ALL 719 | [] 720 | (select* [this structure next-fn] 721 | (n/all-select structure next-fn)) 722 | (transform* [this structure next-fn] 723 | (n/all-transform structure next-fn))) 724 | 725 | (defnav 726 | ^{:doc "Same as ALL, except maintains metadata on the structure."} 727 | ALL-WITH-META 728 | [] 729 | (select* [this structure next-fn] 730 | (n/all-select structure next-fn)) 731 | (transform* [this structure next-fn] 732 | (let [m (meta structure) 733 | res (n/all-transform structure next-fn)] 734 | (if (some? res) 735 | (with-meta res m) 736 | )))) 737 | 738 | (defnav 739 | ^{:doc "Navigate to each value of the map. This is more efficient than 740 | navigating via [ALL LAST]"} 741 | MAP-VALS 742 | [] 743 | (select* [this structure next-fn] 744 | (doseqres NONE [v (vals structure)] 745 | (next-fn v))) 746 | (transform* [this structure next-fn] 747 | (n/map-vals-transform structure next-fn))) 748 | 749 | (defnav 750 | ^{:doc "Navigate to each key of the map. This is more efficient than 751 | navigating via [ALL FIRST]"} 752 | MAP-KEYS 753 | [] 754 | (select* [this structure next-fn] 755 | (doseqres NONE [k (keys structure)] 756 | (next-fn k))) 757 | (transform* [this structure next-fn] 758 | (n/map-keys-transform structure next-fn))) 759 | 760 | 761 | (defcollector VAL [] 762 | (collect-val [this structure] 763 | structure)) 764 | 765 | (def 766 | ^{:doc "Navigate to the last element of the collection. If the collection is 767 | empty navigation is stopped at this point."} 768 | LAST 769 | (n/PosNavigator n/get-last n/update-last)) 770 | 771 | (def 772 | ^{:doc "Navigate to the first element of the collection. If the collection is 773 | empty navigation is stopped at this point."} 774 | FIRST 775 | (n/PosNavigator n/get-first n/update-first)) 776 | 777 | (defnav 778 | ^{:doc "Uses start-index-fn and end-index-fn to determine the bounds of the subsequence 779 | to select when navigating. `start-index-fn` takes in the structure as input. `end-index-fn` 780 | can be one of two forms. If a regular function (e.g. defined with `fn`), it takes in only the structure as input. If a function defined using special `end-fn` macro, it takes in the structure and the result of `start-index-fn`."} 781 | srange-dynamic 782 | [start-index-fn end-index-fn] 783 | (select* [this structure next-fn] 784 | (let [s (start-index-fn structure)] 785 | (n/srange-select structure s (n/invoke-end-fn end-index-fn structure s) next-fn))) 786 | (transform* [this structure next-fn] 787 | (let [s (start-index-fn structure)] 788 | (n/srange-transform structure s (n/invoke-end-fn end-index-fn structure s) next-fn)))) 789 | 790 | 791 | (defnav 792 | ^{:doc "Navigates to the subsequence bound by the indexes start (inclusive) 793 | and end (exclusive)"} 794 | srange 795 | [start end] 796 | (select* [this structure next-fn] 797 | (n/srange-select structure start end next-fn)) 798 | (transform* [this structure next-fn] 799 | (n/srange-transform structure start end next-fn))) 800 | 801 | 802 | (defnav 803 | ^{:doc "Navigates to every continuous subsequence of elements matching `pred`"} 804 | continuous-subseqs 805 | [pred] 806 | (select* [this structure next-fn] 807 | (doseqres NONE [[s e] (i/matching-ranges structure pred)] 808 | (n/srange-select structure s e next-fn))) 809 | (transform* [this structure next-fn] 810 | (i/continuous-subseqs-transform* pred structure next-fn))) 811 | 812 | 813 | (defnav 814 | ^{:doc "Navigate to the empty subsequence before the first element of the collection."} 815 | BEGINNING 816 | [] 817 | (select* [this structure next-fn] 818 | (next-fn (if (string? structure) "" []))) 819 | (transform* [this structure next-fn] 820 | (if (string? structure) 821 | (str (next-fn "") structure) 822 | (let [to-prepend (next-fn [])] 823 | (n/prepend-all structure to-prepend))))) 824 | 825 | 826 | (defnav 827 | ^{:doc "Navigate to the empty subsequence after the last element of the collection."} 828 | END 829 | [] 830 | (select* [this structure next-fn] 831 | (next-fn (if (string? structure) "" []))) 832 | (transform* [this structure next-fn] 833 | (if (string? structure) 834 | (str structure (next-fn "")) 835 | (let [to-append (next-fn [])] 836 | (n/append-all structure to-append))))) 837 | 838 | (defnav 839 | ^{:doc "Navigate to 'void' elem in the set. 840 | For transformations - if result is not `NONE`, 841 | then add that value to the set."} 842 | NONE-ELEM 843 | [] 844 | (select* [this structure next-fn] 845 | (next-fn NONE)) 846 | (transform* [this structure next-fn] 847 | (let [newe (next-fn NONE)] 848 | (if (identical? NONE newe) 849 | structure 850 | (if (nil? structure) 851 | #{newe} 852 | (conj structure newe) 853 | ))))) 854 | 855 | (defnav 856 | ^{:doc "Navigate to 'void' element before the sequence. 857 | For transformations – if result is not `NONE`, 858 | then prepend that value."} 859 | BEFORE-ELEM 860 | [] 861 | (select* [this structure next-fn] 862 | (next-fn NONE)) 863 | (transform* [this structure next-fn] 864 | (let [newe (next-fn NONE)] 865 | (if (identical? NONE newe) 866 | structure 867 | (n/prepend-one structure newe) 868 | )))) 869 | 870 | (defnav 871 | ^{:doc "Navigate to 'void' element after the sequence. 872 | For transformations – if result is not `NONE`, 873 | then append that value."} 874 | AFTER-ELEM 875 | [] 876 | (select* [this structure next-fn] 877 | (next-fn NONE)) 878 | (transform* [this structure next-fn] 879 | (let [newe (next-fn NONE)] 880 | (if (identical? NONE newe) 881 | structure 882 | (n/append-one structure newe) 883 | )))) 884 | 885 | (defnav 886 | ^{:doc "Navigates to the specified subset (by taking an intersection). 887 | In a transform, that subset in the original set is changed to the 888 | new value of the subset."} 889 | subset 890 | [aset] 891 | (select* [this structure next-fn] 892 | (next-fn (set/intersection structure aset))) 893 | (transform* [this structure next-fn] 894 | (let [subset (set/intersection structure aset) 895 | newset (next-fn subset)] 896 | (-> structure 897 | (set/difference subset) 898 | (set/union newset))))) 899 | 900 | 901 | (defnav 902 | ^{:doc "Navigates to the specified submap (using select-keys). 903 | In a transform, that submap in the original map is changed to the new 904 | value of the submap."} 905 | submap 906 | [m-keys] 907 | (select* [this structure next-fn] 908 | (next-fn (select-keys structure m-keys))) 909 | 910 | (transform* [this structure next-fn] 911 | (let [submap (select-keys structure m-keys) 912 | newmap (next-fn submap)] 913 | (merge (reduce dissoc structure m-keys) 914 | newmap)))) 915 | 916 | (defdynamicnav subselect 917 | "Navigates to a sequence that contains the results of (select ...), 918 | but is a view to the original structure that can be transformed. 919 | 920 | Requires that the input navigators will walk the structure's 921 | children in the same order when executed on \"select\" and then 922 | \"transform\". 923 | 924 | If transformed sequence is smaller than input sequence, missing entries 925 | will be filled in with NONE, triggering removal if supported by that navigator. 926 | 927 | Value collection (e.g. collect, collect-one) may not be used in the subpath." 928 | [& path] 929 | (late-bound-nav [late (late-path path)] 930 | (select* [this structure next-fn] 931 | (next-fn (compiled-select late structure))) 932 | (transform* [this structure next-fn] 933 | (let [select-result (compiled-select late structure) 934 | transformed (next-fn select-result) 935 | values-to-insert (i/mutable-cell (seq transformed))] 936 | (compiled-transform late 937 | (fn [_] (let [vs (i/get-cell values-to-insert)] 938 | (if vs 939 | (do (i/update-cell! values-to-insert next) 940 | (first vs)) 941 | NONE 942 | ))) 943 | structure))))) 944 | 945 | (defrichnav 946 | ^{:doc "Navigates to the given key in the map (not to the value). Navigates only if the 947 | key currently exists in the map. Can transform to NONE to remove the key/value 948 | pair from the map."} 949 | map-key 950 | [key] 951 | (select* [this vals structure next-fn] 952 | (if (contains? structure key) 953 | (next-fn vals key) 954 | NONE 955 | )) 956 | (transform* [this vals structure next-fn] 957 | (if (contains? structure key) 958 | (let [newkey (next-fn vals key) 959 | dissoced (dissoc structure key)] 960 | (if (identical? NONE newkey) 961 | dissoced 962 | (assoc dissoced newkey (get structure key)) 963 | )) 964 | structure 965 | ))) 966 | 967 | (defrichnav 968 | ^{:doc "Navigates to the given element in the set only if it exists in the set. 969 | Can transform to NONE to remove the element from the set."} 970 | set-elem 971 | [elem] 972 | (select* [this vals structure next-fn] 973 | (if (contains? structure elem) 974 | (next-fn vals elem) 975 | NONE 976 | )) 977 | (transform* [this vals structure next-fn] 978 | (if (contains? structure elem) 979 | (let [newelem (next-fn vals elem) 980 | removed (disj structure elem)] 981 | (if (identical? NONE newelem) 982 | removed 983 | (conj removed newelem) 984 | )) 985 | structure 986 | ))) 987 | 988 | (def ^{:doc "Navigate to the specified keys one after another. If navigate to NONE, 989 | that element is removed from the map or vector."} 990 | keypath 991 | (eachnav n/keypath*)) 992 | 993 | (def ^{:doc "Navigate to the specified keys one after another, only if they exist 994 | in the data structure. If navigate to NONE, that element is removed 995 | from the map or vector."} 996 | must 997 | (eachnav n/must*)) 998 | 999 | (def ^{:doc "Navigate to the specified indices one after another. If navigate to 1000 | NONE, that element is removed from the sequence."} 1001 | nthpath 1002 | (eachnav n/nthpath*)) 1003 | 1004 | (defrichnav 1005 | ^{:doc "Navigates to the empty space between the index and the prior index. For select 1006 | navigates to NONE, and transforms to non-NONE insert at that position."} 1007 | before-index 1008 | [index] 1009 | (select* [this vals structure next-fn] 1010 | NONE) 1011 | (transform* [this vals structure next-fn] 1012 | (let [v (next-fn vals NONE)] 1013 | (if 1014 | (identical? NONE v) 1015 | structure 1016 | (n/insert-before-idx structure index v))))) 1017 | 1018 | (defrichnav 1019 | ^{:doc "Navigates to the index of the sequence if within 0 and size. Transforms move element 1020 | at that index to the new index, shifting other elements in the sequence."} 1021 | index-nav 1022 | [i] 1023 | (select* [this vals structure next-fn] 1024 | (if (and (>= i 0) (< i (count structure))) 1025 | (next-fn vals i) 1026 | NONE 1027 | )) 1028 | (transform* [this vals structure next-fn] 1029 | (if (and (>= i 0) (< i (count structure))) 1030 | (let [newi (next-fn vals i)] 1031 | (if (= newi i) 1032 | structure 1033 | (let [v (nth structure i)] 1034 | (if (vector? structure) 1035 | (let [shifted (if (< newi i) 1036 | (loop [j (dec i) 1037 | s structure] 1038 | (if (< j newi) 1039 | s 1040 | (recur (dec j) (assoc s (inc j) (nth s j))) 1041 | )) 1042 | (loop [j (inc i) 1043 | s structure] 1044 | (if (> j newi) 1045 | s 1046 | (recur (inc j) (assoc s (dec j) (nth s j))) 1047 | )))] 1048 | (assoc shifted newi v) 1049 | ) 1050 | (->> structure 1051 | (setval (nthpath i) NONE) 1052 | (setval (before-index newi) v) 1053 | ))))) 1054 | structure 1055 | ))) 1056 | 1057 | (defnav 1058 | ^{:doc "Navigate to [index elem] pairs for each element in a sequence. The sequence will be indexed 1059 | starting from `start`. Changing index in transform has same effect as `index-nav`. Indices seen 1060 | during transform take into account any shifting from prior sequence elements changing indices."} 1061 | indexed-vals 1062 | [start] 1063 | (select* [this structure next-fn] 1064 | ;; could be more efficient with a primitive mutable field 1065 | (let [i (i/mutable-cell (dec start))] 1066 | (doseqres NONE [e structure] 1067 | (i/update-cell! i inc) 1068 | (next-fn [(i/get-cell i) e]) 1069 | ))) 1070 | (transform* [this structure next-fn] 1071 | (let [indices (i/mutable-cell (-> structure count range))] 1072 | (reduce 1073 | (fn [s e] 1074 | (let [curri (first (i/get-cell indices)) 1075 | [newi* newe] (next-fn [(+ start curri) e]) 1076 | newi (- newi* start)] 1077 | (i/update-cell! 1078 | indices 1079 | (fn [ii] 1080 | (let [ii2 (next ii)] 1081 | (if (> newi curri) 1082 | (transform [ALL #(>= % (inc curri)) #(<= % newi)] dec ii2) 1083 | ii2 1084 | )))) 1085 | (->> s 1086 | (setval (nthpath curri) newe) 1087 | (setval (index-nav curri) newi) 1088 | ))) 1089 | structure 1090 | structure 1091 | )))) 1092 | 1093 | (def 1094 | ^{:doc "`indexed-vals` with a starting index of 0."} 1095 | INDEXED-VALS 1096 | (indexed-vals 0)) 1097 | 1098 | (defrichnav 1099 | ^{:doc "Navigates to result of running `afn` on the currently navigated value."} 1100 | view 1101 | [afn] 1102 | (select* [this vals structure next-fn] 1103 | (next-fn vals (afn structure))) 1104 | (transform* [this vals structure next-fn] 1105 | (next-fn vals (afn structure)))) 1106 | 1107 | 1108 | (defnav 1109 | ^{:doc "Navigate to the result of running `parse-fn` on the value. For 1110 | transforms, the transformed value then has `unparse-fn` run on 1111 | it to get the final value at this point."} 1112 | parser 1113 | [parse-fn unparse-fn] 1114 | (select* [this structure next-fn] 1115 | (next-fn (parse-fn structure))) 1116 | (transform* [this structure next-fn] 1117 | (unparse-fn (next-fn (parse-fn structure))))) 1118 | 1119 | 1120 | (defnav 1121 | ^{:doc "Navigates to atom value."} 1122 | ATOM 1123 | [] 1124 | (select* [this structure next-fn] 1125 | (next-fn @structure)) 1126 | (transform* [this structure next-fn] 1127 | (do 1128 | (swap! structure next-fn) 1129 | structure))) 1130 | 1131 | (defnav regex-nav [re] 1132 | (select* [this structure next-fn] 1133 | (doseqres NONE [s (re-seq re structure)] 1134 | (next-fn s))) 1135 | (transform* [this structure next-fn] 1136 | (clojure.string/replace structure re next-fn))) 1137 | 1138 | (defdynamicnav selected? 1139 | "Filters the current value based on whether a path finds anything. 1140 | e.g. (selected? :vals ALL even?) keeps the current element only if an 1141 | even number exists for the :vals key." 1142 | [& path] 1143 | (if-let [afn (n/extract-basic-filter-fn path)] 1144 | afn 1145 | (late-bound-richnav [late (late-path path)] 1146 | (select* [this vals structure next-fn] 1147 | (i/filter-select 1148 | #(n/selected?* late vals %) 1149 | vals 1150 | structure 1151 | next-fn)) 1152 | (transform* [this vals structure next-fn] 1153 | (i/filter-transform 1154 | #(n/selected?* late vals %) 1155 | vals 1156 | structure 1157 | next-fn))))) 1158 | 1159 | (defdynamicnav not-selected? [& path] 1160 | (if-let [afn (n/extract-basic-filter-fn path)] 1161 | (fn [s] (not (afn s))) 1162 | (late-bound-richnav [late (late-path path)] 1163 | (select* [this vals structure next-fn] 1164 | (i/filter-select 1165 | #(n/not-selected?* late vals %) 1166 | vals 1167 | structure 1168 | next-fn)) 1169 | (transform* [this vals structure next-fn] 1170 | (i/filter-transform 1171 | #(n/not-selected?* late vals %) 1172 | vals 1173 | structure 1174 | next-fn))))) 1175 | 1176 | (defdynamicnav filterer 1177 | "Navigates to a view of the current sequence that only contains elements that 1178 | match the given path. An element matches the selector path if calling select 1179 | on that element with the path yields anything other than an empty sequence. 1180 | 1181 | For transformation: `NONE` entries in the result sequence cause corresponding entries in 1182 | input to be removed. A result sequence smaller than the input sequence is equivalent to 1183 | padding the result sequence with `NONE` at the end until the same size as the input." 1184 | [& path] 1185 | (subselect ALL (selected? path))) 1186 | 1187 | (defdynamicnav transformed 1188 | "Navigates to a view of the current value by transforming it with the 1189 | specified path and update-fn." 1190 | [path update-fn] 1191 | (late-bound-nav [late (late-path path) 1192 | late-fn update-fn] 1193 | (select* [this structure next-fn] 1194 | (next-fn (compiled-transform late late-fn structure))) 1195 | (transform* [this structure next-fn] 1196 | (next-fn (compiled-transform late late-fn structure))))) 1197 | 1198 | (defdynamicnav traversed 1199 | "Navigates to a view of the current value by transforming with a reduction over 1200 | the specified traversal." 1201 | [path reduce-fn] 1202 | (late-bound-nav [late (late-path path) 1203 | late-fn reduce-fn] 1204 | (select* [this structure next-fn] 1205 | (next-fn (reduce late-fn (compiled-traverse late structure)))) 1206 | (transform* [this structure next-fn] 1207 | (next-fn (reduce late-fn (compiled-traverse late structure))) 1208 | ))) 1209 | 1210 | (def 1211 | ^{:doc "Keeps the element only if it matches the supplied predicate. Functions in paths 1212 | implicitly convert to this navigator." 1213 | :direct-nav true} 1214 | pred 1215 | i/pred*) 1216 | 1217 | 1218 | (defn ^:direct-nav pred= [v] (pred #(= % v))) 1219 | (defn ^:direct-nav pred< [v] (pred #(< % v))) 1220 | (defn ^:direct-nav pred> [v] (pred #(> % v))) 1221 | (defn ^:direct-nav pred<= [v] (pred #(<= % v))) 1222 | (defn ^:direct-nav pred>= [v] (pred #(>= % v))) 1223 | 1224 | (extend-type nil 1225 | ImplicitNav 1226 | (implicit-nav [this] STAY)) 1227 | 1228 | (extend-type #?(:clj clojure.lang.Keyword :cljs cljs.core/Keyword) 1229 | ImplicitNav 1230 | (implicit-nav [this] (n/keypath* this))) 1231 | 1232 | (extend-type #?(:clj clojure.lang.Symbol :cljs cljs.core/Symbol) 1233 | ImplicitNav 1234 | (implicit-nav [this] (n/keypath* this))) 1235 | 1236 | (extend-type #?(:clj String :cljs string) 1237 | ImplicitNav 1238 | (implicit-nav [this] (n/keypath* this))) 1239 | 1240 | (extend-type #?(:clj Number :cljs number) 1241 | ImplicitNav 1242 | (implicit-nav [this] (n/keypath* this))) 1243 | 1244 | (extend-type #?(:clj Character :cljs char) 1245 | ImplicitNav 1246 | (implicit-nav [this] (n/keypath* this))) 1247 | 1248 | (extend-type #?(:clj Boolean :cljs boolean) 1249 | ImplicitNav 1250 | (implicit-nav [this] (n/keypath* this))) 1251 | 1252 | (extend-type #?(:clj clojure.lang.AFn :cljs function) 1253 | ImplicitNav 1254 | (implicit-nav [this] (pred this))) 1255 | 1256 | (extend-type #?(:clj clojure.lang.PersistentHashSet :cljs cljs.core/PersistentHashSet) 1257 | ImplicitNav 1258 | (implicit-nav [this] (pred this))) 1259 | 1260 | (extend-type #?(:clj java.util.regex.Pattern :cljs js/RegExp) 1261 | ImplicitNav 1262 | (implicit-nav [this] (regex-nav this))) 1263 | 1264 | (defnav 1265 | ^{:doc "Navigates to the provided val if the structure is nil. Otherwise it stays 1266 | navigated at the structure."} 1267 | nil->val 1268 | [v] 1269 | (select* [this structure next-fn] 1270 | (next-fn (if (nil? structure) v structure))) 1271 | (transform* [this structure next-fn] 1272 | (next-fn (if (nil? structure) v structure)))) 1273 | 1274 | (def 1275 | ^{:doc "Navigates to #{} if the value is nil. Otherwise it stays 1276 | navigated at the current value."} 1277 | NIL->SET 1278 | (nil->val #{})) 1279 | 1280 | (def 1281 | ^{:doc "Navigates to '() if the value is nil. Otherwise it stays 1282 | navigated at the current value."} 1283 | NIL->LIST 1284 | (nil->val '())) 1285 | 1286 | (def 1287 | ^{:doc "Navigates to [] if the value is nil. Otherwise it stays 1288 | navigated at the current value."} 1289 | NIL->VECTOR 1290 | (nil->val [])) 1291 | 1292 | (defnav ^{:doc "Navigates to the metadata of the structure, or nil if 1293 | the structure has no metadata or may not contain metadata."} 1294 | META 1295 | [] 1296 | (select* [this structure next-fn] 1297 | (next-fn (meta structure))) 1298 | (transform* [this structure next-fn] 1299 | (with-meta structure (next-fn (meta structure))))) 1300 | 1301 | (defnav ^{:doc "Navigates to the name portion of the keyword or symbol"} 1302 | NAME 1303 | [] 1304 | (select* [this structure next-fn] 1305 | (next-fn (name structure))) 1306 | (transform* [this structure next-fn] 1307 | (let [new-name (next-fn (name structure)) 1308 | ns (namespace structure)] 1309 | (cond (keyword? structure) (keyword ns new-name) 1310 | (symbol? structure) (symbol ns new-name) 1311 | :else (throw (ex-info "NAME can only be used on symbols or keywords" {:structure structure})) 1312 | )))) 1313 | 1314 | (defnav ^{:doc "Navigates to the namespace portion of the keyword or symbol"} 1315 | NAMESPACE 1316 | [] 1317 | (select* [this structure next-fn] 1318 | (next-fn (namespace structure))) 1319 | (transform* [this structure next-fn] 1320 | (let [name (name structure) 1321 | new-ns (next-fn (namespace structure))] 1322 | (cond (keyword? structure) (keyword new-ns name) 1323 | (symbol? structure) (symbol new-ns name) 1324 | :else (throw (ex-info "NAMESPACE can only be used on symbols or keywords" 1325 | {:structure structure})) 1326 | )))) 1327 | 1328 | (defdynamicnav 1329 | ^{:doc "Adds the result of running select with the given path on the 1330 | current value to the collected vals."} 1331 | collect 1332 | [& path] 1333 | (late-bound-collector [late (late-path path)] 1334 | (collect-val [this structure] 1335 | (compiled-select late structure)))) 1336 | 1337 | 1338 | (defdynamicnav 1339 | ^{:doc "Adds the result of running select-one with the given path on the 1340 | current value to the collected vals."} 1341 | collect-one 1342 | [& path] 1343 | (late-bound-collector [late (late-path path)] 1344 | (collect-val [this structure] 1345 | (compiled-select-one late structure)))) 1346 | 1347 | 1348 | (defcollector 1349 | ^{:doc 1350 | "Adds an external value to the collected vals. Useful when additional arguments 1351 | are required to the transform function that would otherwise require partial 1352 | application or a wrapper function. 1353 | 1354 | e.g., incrementing val at path [:a :b] by 3: 1355 | (transform [:a :b (putval 3)] + some-map)"} 1356 | putval 1357 | [val] 1358 | (collect-val [this structure] 1359 | val)) 1360 | 1361 | (defdynamicnav 1362 | ^{:doc 1363 | "Continues navigating on the given path with the collected vals reset to []. Once 1364 | navigation leaves the scope of with-fresh-collected, the collected vals revert 1365 | to what they were before."} 1366 | with-fresh-collected 1367 | [& path] 1368 | (late-bound-richnav [late (late-path path)] 1369 | (select* [this vals structure next-fn] 1370 | (i/exec-select* late [] structure (fn [_ structure] (next-fn vals structure))) 1371 | ) 1372 | (transform* [this vals structure next-fn] 1373 | (i/exec-transform* late [] structure (fn [_ structure] (next-fn vals structure)))) 1374 | )) 1375 | 1376 | (defrichnav 1377 | ^{:doc "Drops all collected values for subsequent navigation."} 1378 | DISPENSE 1379 | [] 1380 | (select* [this vals structure next-fn] 1381 | (next-fn [] structure)) 1382 | (transform* [this vals structure next-fn] 1383 | (next-fn [] structure))) 1384 | 1385 | (defdynamicnav if-path 1386 | "Like cond-path, but with if semantics." 1387 | ([cond-p then-path] 1388 | (if-path cond-p then-path STOP)) 1389 | ([cond-p then-path else-path] 1390 | (if-let [afn (n/extract-basic-filter-fn cond-p)] 1391 | (late-bound-richnav [late-then (late-path then-path) 1392 | late-else (late-path else-path)] 1393 | (select* [this vals structure next-fn] 1394 | (n/if-select 1395 | vals 1396 | structure 1397 | next-fn 1398 | afn 1399 | late-then 1400 | late-else)) 1401 | (transform* [this vals structure next-fn] 1402 | (n/if-transform 1403 | vals 1404 | structure 1405 | next-fn 1406 | afn 1407 | late-then 1408 | late-else))) 1409 | (late-bound-richnav [late-cond (late-path cond-p) 1410 | late-then (late-path then-path) 1411 | late-else (late-path else-path)] 1412 | (select* [this vals structure next-fn] 1413 | (n/if-select 1414 | vals 1415 | structure 1416 | next-fn 1417 | #(n/selected?* late-cond vals %) 1418 | late-then 1419 | late-else)) 1420 | (transform* [this vals structure next-fn] 1421 | (n/if-transform 1422 | vals 1423 | structure 1424 | next-fn 1425 | #(n/selected?* late-cond vals %) 1426 | late-then 1427 | late-else)))))) 1428 | 1429 | 1430 | (defdynamicnav cond-path 1431 | "Takes in alternating cond-path path cond-path path... 1432 | Tests the structure if selecting with cond-path returns anything. 1433 | If so, it uses the following path for this portion of the navigation. 1434 | Otherwise, it tries the next cond-path. If nothing matches, then the structure 1435 | is not selected." 1436 | [& conds] 1437 | (let [pairs (reverse (partition 2 conds))] 1438 | (reduce 1439 | (fn [p [tester apath]] 1440 | (if-path tester apath p)) 1441 | STOP 1442 | pairs))) 1443 | 1444 | 1445 | (defdynamicnav multi-path 1446 | "A path that branches on multiple paths. For updates, 1447 | applies updates to the paths in order." 1448 | ([] STAY) 1449 | ([path] path) 1450 | ([path1 path2] 1451 | (late-bound-richnav [late1 (late-path path1) 1452 | late2 (late-path path2)] 1453 | (select* [this vals structure next-fn] 1454 | (let [res1 (i/exec-select* late1 vals structure next-fn)] 1455 | (if (reduced? res1) 1456 | res1 1457 | (let [res2 (i/exec-select* late2 vals structure next-fn)] 1458 | (if (identical? NONE res1) 1459 | res2 1460 | res1 1461 | ))))) 1462 | (transform* [this vals structure next-fn] 1463 | (let [s1 (i/exec-transform* late1 vals structure next-fn)] 1464 | (i/exec-transform* late2 vals s1 next-fn))))) 1465 | ([path1 path2 & paths] 1466 | (reduce multi-path (multi-path path1 path2) paths))) 1467 | 1468 | 1469 | (defdynamicnav stay-then-continue 1470 | "Navigates to the current element and then navigates via the provided path. 1471 | This can be used to implement pre-order traversal." 1472 | [& path] 1473 | (multi-path STAY path)) 1474 | 1475 | (defdynamicnav continue-then-stay 1476 | "Navigates to the provided path and then to the current element. This can be used 1477 | to implement post-order traversal." 1478 | [& path] 1479 | (multi-path path STAY)) 1480 | 1481 | (def 1482 | ^{:doc "Navigate the data structure until reaching 1483 | a value for which `afn` returns truthy. Has 1484 | same semantics as clojure.walk."} 1485 | walker 1486 | (recursive-path [afn] p 1487 | (cond-path (pred afn) STAY 1488 | coll? [ALL p] 1489 | ))) 1490 | 1491 | (def 1492 | ^{:doc "Like `walker` but maintains metadata of any forms traversed."} 1493 | codewalker 1494 | (recursive-path [afn] p 1495 | (cond-path (pred afn) STAY 1496 | coll? [ALL-WITH-META p] 1497 | ))) 1498 | 1499 | (let [empty->NONE (if-path empty? (terminal-val NONE)) 1500 | compact* (fn [nav] (multi-path nav empty->NONE))] 1501 | (defdynamicnav compact 1502 | "During transforms, after each step of navigation in subpath check if the 1503 | value is empty. If so, remove that value by setting it to NONE." 1504 | [& path] 1505 | (map compact* path) 1506 | )) 1507 | -------------------------------------------------------------------------------- /src/clj/com/rpl/specter/impl.cljc: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.impl 2 | #?(:cljs (:require-macros 3 | [com.rpl.specter.util-macros 4 | :refer [doseqres mk-comp-navs mk-late-fn mk-late-fn-records]] 5 | )) 6 | ;; workaround for cljs bug that emits warnings for vars named the same as a 7 | ;; private var in cljs.core (in this case `NONE`, added as private var to 8 | ;; cljs.core with 1.9.562) 9 | #?(:cljs (:refer-clojure :exclude [NONE])) 10 | (:use [com.rpl.specter.protocols :only 11 | [select* transform* collect-val RichNavigator]] 12 | #?(:clj [com.rpl.specter.util-macros :only [doseqres mk-comp-navs]])) 13 | 14 | (:require [com.rpl.specter.protocols :as p] 15 | #?(:clj [clojure.pprint :as pp]) 16 | [clojure.string :as s] 17 | [clojure.walk :as walk] 18 | #?(:bb [clojure.walk :as riddley] 19 | :clj [riddley.walk :as riddley])) 20 | 21 | #?@(:bb [] 22 | :clj [(:import [com.rpl.specter Util MutableCell])])) 23 | 24 | (def NONE ::NONE) 25 | 26 | (defn spy [e] 27 | (println "SPY:") 28 | (println (pr-str e)) 29 | e) 30 | 31 | (defn- smart-str* [o] 32 | (if (coll? o) 33 | (pr-str o) 34 | (str o))) 35 | 36 | (defn ^String smart-str [& elems] 37 | (apply str (map smart-str* elems))) 38 | 39 | (defn fast-constantly [v] 40 | (fn ([] v) 41 | ([a1] v) 42 | ([a1 a2] v) 43 | ([a1 a2 a3] v) 44 | ([a1 a2 a3 a4] v) 45 | ([a1 a2 a3 a4 a5] v) 46 | ([a1 a2 a3 a4 a5 a6] v) 47 | ([a1 a2 a3 a4 a5 a6 a7] v) 48 | ([a1 a2 a3 a4 a5 a6 a7 a8] v) 49 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9] v) 50 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] v) 51 | ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 & r] v))) 52 | 53 | 54 | ;; need to get the expansion function like this so that 55 | ;; this code compiles in a clojure environment where cljs.analyzer 56 | ;; namespace does not exist 57 | #?( 58 | :clj 59 | (defn cljs-analyzer-macroexpand-1 [] 60 | (eval 'cljs.analyzer/macroexpand-1)) 61 | 62 | ;; this version is for bootstrap cljs 63 | :cljs 64 | (defn cljs-analyzer-macroexpand-1 [] 65 | ^:cljs.analyzer/no-resolve cljs.analyzer/macroexpand-1)) 66 | 67 | 68 | #?( 69 | :clj 70 | (defn clj-macroexpand-all [form] 71 | (riddley/macroexpand-all form)) 72 | 73 | :cljs 74 | (defn clj-macroexpand-all [form] 75 | (throw (ex-info "not implemented" {})))) 76 | 77 | 78 | #?( 79 | :clj 80 | (defn intern* [ns name val] (intern ns name val)) 81 | 82 | :cljs 83 | (defn intern* [ns name val] 84 | (throw (ex-info "intern not supported in ClojureScript" {})))) 85 | 86 | #?(:bb 87 | (defmacro fast-object-array [i] 88 | `(object-array ~i)) 89 | :clj 90 | (defmacro fast-object-array [i] 91 | `(com.rpl.specter.Util/makeObjectArray ~i))) 92 | 93 | 94 | (defn benchmark [iters afn] 95 | (time 96 | (dotimes [_ iters] 97 | (afn)))) 98 | 99 | #?( 100 | :clj 101 | (defmacro exec-select* [this & args] 102 | (let [platform (if (contains? &env :locals) :cljs :clj) 103 | hinted (with-meta (gensym) {:tag 'com.rpl.specter.protocols.RichNavigator})] 104 | (if (= platform :cljs) 105 | `(p/select* ~this ~@args) 106 | `(let [~hinted ~this] 107 | (#?(:bb p/select* 108 | :clj .select*) ~hinted ~@args))))) 109 | :cljs 110 | (defn exec-select* [this vals structure next-fn] 111 | (p/select* ^not-native this vals structure next-fn))) 112 | 113 | 114 | #?( 115 | :clj 116 | (defmacro exec-transform* [this & args] 117 | (let [platform (if (contains? &env :locals) :cljs :clj) 118 | hinted (with-meta (gensym) {:tag 'com.rpl.specter.protocols.RichNavigator})] 119 | (if (= platform :cljs) 120 | `(p/transform* ~this ~@args) 121 | `(let [~hinted ~this] 122 | (#?(:bb p/transform* 123 | :clj .transform*) ~hinted ~@args))))) 124 | 125 | :cljs 126 | (defn exec-transform* [this vals structure next-fn] 127 | (p/transform* ^not-native this vals structure next-fn))) 128 | 129 | (defprotocol PathComposer 130 | (do-comp-paths [paths])) 131 | 132 | (defn rich-nav? [n] 133 | #?(:clj (instance? com.rpl.specter.protocols.RichNavigator n) 134 | :cljs (satisfies? RichNavigator n))) 135 | 136 | (defn comp-paths* [p] 137 | (if (rich-nav? p) p (do-comp-paths p))) 138 | 139 | (defn- coerce-object [this] 140 | (cond (rich-nav? this) this 141 | (satisfies? p/ImplicitNav this) (p/implicit-nav this) 142 | :else (throw (ex-info "Not a navigator" 143 | {:this this 144 | :type-str (pr-str (type this))})))) 145 | 146 | 147 | (defprotocol CoercePath 148 | (coerce-path [this])) 149 | 150 | (extend-protocol CoercePath 151 | nil ; needs its own coercer because it doesn't count as an Object 152 | (coerce-path [this] 153 | (coerce-object this)) 154 | 155 | #?(:clj java.util.List :cljs cljs.core/PersistentVector) 156 | (coerce-path [this] 157 | (do-comp-paths this)) 158 | 159 | #?(:cljs cljs.core/IndexedSeq) 160 | #?(:cljs (coerce-path [this] 161 | (coerce-path (vec this)))) 162 | #?(:cljs cljs.core/EmptyList) 163 | #?(:cljs (coerce-path [this] 164 | (coerce-path (vec this)))) 165 | #?(:cljs cljs.core/List) 166 | #?(:cljs (coerce-path [this] 167 | (coerce-path (vec this)))) 168 | #?(:cljs cljs.core/LazySeq) 169 | #?(:cljs (coerce-path [this] 170 | (coerce-path (vec this)))) 171 | #?(:cljs cljs.core/Subvec) 172 | #?(:cljs (coerce-path [this] 173 | (coerce-path (into [] this)))) 174 | 175 | #?(:clj Object :cljs default) 176 | (coerce-path [this] 177 | (coerce-object this))) 178 | 179 | (def STAY* 180 | (reify RichNavigator 181 | (select* [this vals structure next-fn] 182 | (next-fn vals structure)) 183 | (transform* [this vals structure next-fn] 184 | (next-fn vals structure)))) 185 | 186 | (defn combine-two-navs [nav1 nav2] 187 | (reify RichNavigator 188 | (select* [this vals structure next-fn] 189 | (exec-select* nav1 vals structure 190 | (fn [vals-next structure-next] 191 | (exec-select* nav2 vals-next structure-next next-fn)))) 192 | (transform* [this vals structure next-fn] 193 | (exec-transform* nav1 vals structure 194 | (fn [vals-next structure-next] 195 | (exec-transform* nav2 vals-next structure-next next-fn)))))) 196 | 197 | (extend-protocol PathComposer 198 | nil 199 | (do-comp-paths [o] 200 | (coerce-path o)) 201 | #?(:clj Object :cljs default) 202 | (do-comp-paths [o] 203 | (coerce-path o)) 204 | #?(:clj java.util.List :cljs cljs.core/PersistentVector) 205 | (do-comp-paths [navigators] 206 | (let [coerced (map coerce-path navigators)] 207 | (cond (empty? coerced) 208 | STAY* 209 | 210 | (= 1 (count coerced)) 211 | (first coerced) 212 | 213 | :else 214 | (reduce combine-two-navs coerced))))) 215 | 216 | ;; cell implementation idea taken from prismatic schema library 217 | #?(:cljs 218 | (defprotocol PMutableCell 219 | (set_cell [cell x]))) 220 | 221 | 222 | #?(:bb 223 | (defrecord MutableCell [x]) 224 | :cljs 225 | (deftype MutableCell [^:volatile-mutable q] 226 | PMutableCell 227 | (set_cell [this x] (set! q x)))) 228 | 229 | 230 | #?(:bb 231 | (defn mutable-cell 232 | ([] (mutable-cell nil)) 233 | ([v] (MutableCell. (volatile! v)))) 234 | 235 | :clj 236 | (defn mutable-cell 237 | ([] (mutable-cell nil)) 238 | ([v] (MutableCell. v))) 239 | 240 | :cljs 241 | (defn mutable-cell 242 | ([] (mutable-cell nil)) 243 | ([init] (MutableCell. init)))) 244 | 245 | 246 | #?(:bb 247 | (defn set-cell! [^MutableCell c v] 248 | (vreset! (:x c) v)) 249 | 250 | :clj 251 | (defn set-cell! [^MutableCell c v] 252 | (.set c v)) 253 | 254 | :cljs 255 | (defn set-cell! [cell val] 256 | (set_cell cell val))) 257 | 258 | 259 | #?(:bb 260 | (defn get-cell [^MutableCell c] 261 | @(:x c)) 262 | 263 | :clj 264 | (defn get-cell [^MutableCell c] 265 | (.get c)) 266 | 267 | 268 | :cljs 269 | (defn get-cell [cell] 270 | (.-q cell))) 271 | 272 | 273 | 274 | 275 | (defn update-cell! [cell afn] 276 | (let [ret (afn (get-cell cell))] 277 | (set-cell! cell ret) 278 | ret)) 279 | 280 | #?( 281 | :clj 282 | (defmacro compiled-traverse-with-vals* [path result-fn vals structure] 283 | `(exec-select* 284 | ~path 285 | ~vals 286 | ~structure 287 | (fn [vals# structure#] 288 | (if (identical? vals# []) 289 | (~result-fn structure#) 290 | (~result-fn (conj vals# structure#)))))) 291 | 292 | :cljs 293 | (defn compiled-traverse-with-vals* [path result-fn vals structure] 294 | (exec-select* 295 | path 296 | vals 297 | structure 298 | (fn [vals structure] 299 | (if (identical? vals []) 300 | (result-fn structure) 301 | (result-fn (conj vals structure))))))) 302 | 303 | 304 | (defn compiled-traverse* [path result-fn structure] 305 | (compiled-traverse-with-vals* path result-fn [] structure)) 306 | 307 | (defn do-compiled-traverse* [apath structure] 308 | (reify #?(:clj clojure.lang.IReduce :cljs cljs.core/IReduce) 309 | (#?(:clj reduce :cljs -reduce) 310 | [this afn] 311 | #?(:bb (reduce afn (afn) this) 312 | :default 313 | (#?(:clj .reduce :cljs -reduce) this afn (afn)))) 314 | (#?(:clj reduce :cljs -reduce) 315 | [this afn start] 316 | (let [cell (mutable-cell start)] 317 | (compiled-traverse* 318 | apath 319 | (fn [elem] 320 | (let [curr (get-cell cell) 321 | newv (afn curr elem)] 322 | (set-cell! cell newv) 323 | newv ; to support reduced handling during traverse 324 | )) 325 | structure) 326 | (get-cell cell) 327 | )))) 328 | 329 | #?( 330 | :bb 331 | (defn- call-reduce-interface [^clojure.lang.IReduce traverser afn start] 332 | (reduce afn start traverser)) 333 | :clj 334 | (defn- call-reduce-interface [^clojure.lang.IReduce traverser afn start] 335 | (.reduce traverser afn start) 336 | ) 337 | 338 | :cljs 339 | (defn- call-reduce-interface [^cljs.core/IReduce traverser afn start] 340 | (-reduce traverser afn start) 341 | )) 342 | 343 | (defn do-compiled-traverse [apath structure] 344 | (let [traverser (do-compiled-traverse* apath structure)] 345 | (reify #?(:clj clojure.lang.IReduce :cljs cljs.core/IReduce) 346 | (#?(:clj reduce :cljs -reduce) 347 | [this afn] 348 | #?(:bb (reduce afn (afn) this) 349 | :default 350 | (#?(:clj .reduce :cljs -reduce) this afn (afn)))) 351 | (#?(:clj reduce :cljs -reduce) 352 | [this afn start] 353 | (let [res (call-reduce-interface traverser afn start)] 354 | (unreduced res) 355 | ))))) 356 | 357 | (defn compiled-traverse-all* [path] 358 | (fn [xf] 359 | (fn 360 | ([] (xf)) 361 | ([result] (xf result)) 362 | ([result input] 363 | (reduce 364 | (fn [r i] 365 | (xf r i)) 366 | result 367 | ;; use this one to make sure reduced vals are propagated back 368 | (do-compiled-traverse* path input) 369 | ) 370 | )))) 371 | 372 | (defn compiled-select* [path structure] 373 | (let [res (mutable-cell (transient [])) 374 | result-fn (fn [structure] 375 | (let [curr (get-cell res)] 376 | (set-cell! res (conj! curr structure))))] 377 | (compiled-traverse* path result-fn structure) 378 | (persistent! (get-cell res)))) 379 | 380 | 381 | (defn compiled-select-one* [path structure] 382 | (let [res (mutable-cell NONE) 383 | result-fn (fn [structure] 384 | (let [curr (get-cell res)] 385 | (if (identical? curr NONE) 386 | (set-cell! res structure) 387 | (throw (ex-info "More than one element found in structure" 388 | {:structure structure})))))] 389 | 390 | (compiled-traverse* path result-fn structure) 391 | (let [ret (get-cell res)] 392 | (if (identical? ret NONE) 393 | nil 394 | ret)))) 395 | 396 | 397 | (defn compiled-select-one!* [path structure] 398 | (let [res (mutable-cell NONE) 399 | result-fn (fn [structure] 400 | (let [curr (get-cell res)] 401 | (if (identical? curr NONE) 402 | (set-cell! res structure) 403 | (throw (ex-info "More than one element found in structure" 404 | {:structure structure})))))] 405 | (compiled-traverse* path result-fn structure) 406 | (let [ret (get-cell res)] 407 | (if (identical? NONE ret) 408 | (throw (ex-info "Found no elements for select-one!" {:structure structure}))) 409 | ret))) 410 | 411 | 412 | 413 | (defn compiled-select-any* 414 | ([path structure] (compiled-select-any* path [] structure)) 415 | ([path vals structure] 416 | (unreduced (compiled-traverse-with-vals* path reduced vals structure)))) 417 | 418 | (defn compiled-select-first* [path structure] 419 | (let [ret (compiled-select-any* path structure)] 420 | (if (identical? ret NONE) 421 | nil 422 | ret 423 | ))) 424 | 425 | (defn compiled-selected-any?* [path structure] 426 | (not (identical? NONE (compiled-select-any* path structure)))) 427 | 428 | (defn terminal* [afn vals structure] 429 | (if (identical? vals []) 430 | (afn structure) 431 | (apply afn (conj vals structure)))) 432 | 433 | ;;TODO: could inline cache the transform-fn, or even have a different one 434 | ;;if know there are no vals at the end 435 | (defn compiled-transform* [nav transform-fn structure] 436 | (exec-transform* nav [] structure 437 | (fn [vals structure] 438 | (terminal* transform-fn vals structure)))) 439 | 440 | (defn compiled-vtransform* [nav transform-fn structure] 441 | (exec-transform* nav [] structure transform-fn)) 442 | 443 | (defn fn-invocation? [f] 444 | (or #?(:clj (instance? clojure.lang.Cons f)) 445 | #?(:clj (instance? clojure.lang.LazySeq f)) 446 | #?(:cljs (instance? cljs.core.LazySeq f)) 447 | (list? f))) 448 | 449 | (defrecord LocalSym 450 | [val sym]) 451 | 452 | ;; needs to be named "avar" instead of "var" due to regression in cljs circa 453 | ;; 6/26/2017. See https://github.com/nathanmarz/specter/issues/215 454 | (defrecord VarUse 455 | [val avar sym]) 456 | 457 | (defrecord SpecialFormUse 458 | [val code]) 459 | 460 | (defrecord FnInvocation 461 | ;; op and params elems can be any of the above 462 | [op params code]) 463 | 464 | (defrecord DynamicVal 465 | [code]) 466 | 467 | (defrecord DynamicPath 468 | [path]) 469 | 470 | (defrecord DynamicFunction 471 | [op params code]) 472 | 473 | (defn dynamic-param? [o] 474 | (contains? #{DynamicPath DynamicVal DynamicFunction} (type o))) 475 | 476 | (defn static-path? [path] 477 | (if (sequential? path) 478 | (every? static-path? path) 479 | (-> path dynamic-param? not))) 480 | 481 | (defn late-path [path] 482 | (if (static-path? path) 483 | (comp-paths* path) 484 | (com.rpl.specter.impl/->DynamicPath path))) 485 | 486 | 487 | 488 | (defrecord CachedPathInfo 489 | [dynamic? precompiled]) 490 | 491 | 492 | ;; these are defined to avoid having to type-hint the CachedPathInfo 493 | ;; in com.rpl.specter/path, which causes problems during aot/uberjar 494 | ;; (clojure seems to be defining CachedPathInfo multiple times) 495 | (defn cached-path-info-precompiled [^CachedPathInfo c] 496 | (.-precompiled c)) 497 | 498 | (defn cached-path-info-dynamic? [^CachedPathInfo c] 499 | (.-dynamic? c)) 500 | 501 | 502 | (defn filter-select [afn vals structure next-fn] 503 | (if (afn structure) 504 | (next-fn vals structure) 505 | NONE)) 506 | 507 | (defn filter-transform [afn vals structure next-fn] 508 | (if (afn structure) 509 | (next-fn vals structure) 510 | structure)) 511 | 512 | (defn ^:direct-nav pred* [afn] 513 | (reify RichNavigator 514 | (select* [this vals structure next-fn] 515 | (if (afn structure) 516 | (next-fn vals structure) 517 | NONE)) 518 | (transform* [this vals structure next-fn] 519 | (if (afn structure) 520 | (next-fn vals structure) 521 | structure)))) 522 | 523 | (defn ^:direct-nav collected?* [afn] 524 | (reify RichNavigator 525 | (select* [this vals structure next-fn] 526 | (if (afn vals) 527 | (next-fn vals structure) 528 | NONE)) 529 | (transform* [this vals structure next-fn] 530 | (if (afn vals) 531 | (next-fn vals structure) 532 | structure)))) 533 | 534 | (defn ^:direct-nav cell-nav [cell] 535 | (reify RichNavigator 536 | (select* [this vals structure next-fn] 537 | (exec-select* (get-cell cell) vals structure next-fn)) 538 | (transform* [this vals structure next-fn] 539 | (exec-transform* (get-cell cell) vals structure next-fn)))) 540 | 541 | (defn local-declarepath [] 542 | (let [cell (mutable-cell nil)] 543 | (vary-meta (cell-nav cell) assoc ::cell cell))) 544 | 545 | (defn providepath* [declared compiled-path] 546 | (let [cell (-> declared meta ::cell)] 547 | (set-cell! cell compiled-path))) 548 | 549 | 550 | 551 | (defn- gensyms [amt] 552 | (vec (repeatedly amt gensym))) 553 | 554 | (mk-comp-navs) 555 | 556 | (defn srange-transform* [structure start end next-fn] 557 | (if (string? structure) 558 | (let [newss (next-fn (subs structure start end))] 559 | (str (subs structure 0 start) 560 | newss 561 | (subs structure end (count structure)) 562 | )) 563 | (let [structurev (vec structure) 564 | newpart (next-fn (-> structurev (subvec start end))) 565 | res (concat (subvec structurev 0 start) 566 | newpart 567 | (subvec structurev end (count structure)))] 568 | (if (vector? structure) 569 | (vec res) 570 | res 571 | )))) 572 | 573 | (defn- matching-indices [aseq p] 574 | (keep-indexed (fn [i e] (if (p e) i)) aseq)) 575 | 576 | (defn matching-ranges [aseq p] 577 | (first 578 | (reduce 579 | (fn [[ranges curr-start curr-last :as curr] i] 580 | (cond 581 | (nil? curr-start) 582 | [ranges i i] 583 | 584 | (= i (inc curr-last)) 585 | [ranges curr-start i] 586 | 587 | :else 588 | [(conj ranges [curr-start (inc curr-last)]) i i])) 589 | 590 | [[] nil nil] 591 | (concat (matching-indices aseq p) [-1])))) 592 | 593 | (defn continuous-subseqs-transform* [pred structure next-fn] 594 | (reduce 595 | (fn [structure [s e]] 596 | (srange-transform* structure s e next-fn)) 597 | structure 598 | (reverse (matching-ranges structure pred)))) 599 | 600 | (defn codewalk-until [pred on-match-fn structure] 601 | (if (pred structure) 602 | (on-match-fn structure) 603 | (let [ret (walk/walk (partial codewalk-until pred on-match-fn) identity structure)] 604 | (if (and (fn-invocation? structure) (fn-invocation? ret)) 605 | (with-meta ret (meta structure)) 606 | ret)))) 607 | 608 | (defn walk-select [pred continue-fn structure] 609 | (let [ret (mutable-cell NONE) 610 | walker (fn this [structure] 611 | (if (pred structure) 612 | (let [r (continue-fn structure)] 613 | (if-not (identical? r NONE) 614 | (set-cell! ret r)) 615 | r) 616 | 617 | (walk/walk this identity structure)))] 618 | 619 | (walker structure) 620 | (get-cell ret))) 621 | 622 | (defn walk-until [pred on-match-fn structure] 623 | (if (pred structure) 624 | (on-match-fn structure) 625 | (walk/walk (partial walk-until pred on-match-fn) identity structure))) 626 | 627 | 628 | #?(:clj 629 | (do 630 | (def ^:dynamic *tmp-closure*) 631 | (defn closed-code [closure body] 632 | (let [lv (mapcat #(vector % `(*tmp-closure* '~%)) 633 | (keys closure))] 634 | (binding [*tmp-closure* closure] 635 | (eval `(let [~@lv] ~body))))) 636 | 637 | 638 | (let [embeddable? (some-fn number? 639 | symbol? 640 | keyword? 641 | string? 642 | char? 643 | list? 644 | vector? 645 | set? 646 | #(and (map? %) (not (record? %))) 647 | nil? 648 | #(instance? clojure.lang.Cons %) 649 | #(instance? clojure.lang.LazySeq %))] 650 | (defn eval+ 651 | "Automatically extracts non-evalable stuff into a closure and then evals" 652 | [form] 653 | (let [replacements (mutable-cell {}) 654 | new-form (codewalk-until 655 | #(-> % embeddable? not) 656 | (fn [o] 657 | (let [s (gensym)] 658 | (update-cell! replacements #(assoc % s o)) 659 | s)) 660 | form) 661 | closure (get-cell replacements)] 662 | (closed-code closure new-form)))))) 663 | 664 | (defn coerce-nav [o] 665 | (cond #?(:clj (instance? com.rpl.specter.protocols.RichNavigator o) 666 | :cljs (satisfies? RichNavigator o)) 667 | o 668 | 669 | (sequential? o) 670 | (comp-paths* o) 671 | 672 | :else 673 | (p/implicit-nav o))) 674 | 675 | 676 | (defn dynamic-var? [v] 677 | (-> v meta :dynamic)) 678 | 679 | ;; original-obj stuff is done to avoid using functions with metadata on them 680 | ;; clojure's implementation of function metadata causes the function to do an 681 | ;; apply for every invocation 682 | (defn direct-nav-obj [o] 683 | (vary-meta o merge {:direct-nav true :original-obj o})) 684 | 685 | (defn maybe-direct-nav [obj direct-nav?] 686 | (if direct-nav? 687 | (direct-nav-obj obj) 688 | obj)) 689 | 690 | (defn original-obj [o] 691 | (let [orig (-> o meta :original-obj)] 692 | (if orig 693 | (recur orig) 694 | o))) 695 | 696 | (defn direct-nav? [o] 697 | (-> o meta :direct-nav)) 698 | 699 | (defn all-static? [params] 700 | (identical? NONE (walk-select dynamic-param? identity params))) 701 | 702 | (defn late-resolved-fn [afn] 703 | (fn [& args] 704 | (if (all-static? args) 705 | (apply afn args) 706 | (->DynamicFunction afn args nil) 707 | ))) 708 | 709 | (defn preserve-map [afn o] 710 | (if (or (list? o) (seq? o)) 711 | (map afn o) 712 | (into (empty o) (map afn o)))) 713 | 714 | (defn- magic-precompilation* [o] 715 | (cond (sequential? o) 716 | (preserve-map magic-precompilation* o) 717 | 718 | (instance? VarUse o) 719 | (let [v (:avar o)] 720 | ;; v can be nil if the symbol referred to an imported class 721 | (if (and v (dynamic-var? v)) 722 | (->DynamicVal (maybe-direct-nav 723 | (:sym o) 724 | (or (direct-nav? v) 725 | (-> o :sym direct-nav?)))) 726 | (maybe-direct-nav 727 | (:val o) 728 | (or (and v (direct-nav? v)) 729 | (-> o :sym direct-nav?) 730 | (-> o :val direct-nav?))))) 731 | 732 | (instance? LocalSym o) 733 | (->DynamicVal (:sym o)) 734 | 735 | (instance? SpecialFormUse o) 736 | (->DynamicVal (:code o)) 737 | 738 | (instance? FnInvocation o) 739 | (let [op (magic-precompilation* (:op o)) 740 | params (doall (map magic-precompilation* (:params o)))] 741 | (if (or (-> op meta :dynamicnav) 742 | (all-static? (conj params op))) 743 | (magic-precompilation* (apply op params)) 744 | (->DynamicFunction op params (:code o)))) 745 | 746 | :else 747 | ;; this handles dynamicval as well 748 | o)) 749 | 750 | (defn static-combine 751 | ([o] (static-combine o true)) 752 | ([o nav-pos?] 753 | (cond (sequential? o) 754 | (if nav-pos? 755 | (let [res (continuous-subseqs-transform* 756 | rich-nav? 757 | (doall (map static-combine (flatten o))) 758 | (fn [s] [(comp-paths* s)]))] 759 | (if (= 1 (count res)) 760 | (first res) 761 | res)) 762 | (preserve-map #(static-combine % false) o)) 763 | 764 | (instance? DynamicFunction o) 765 | (->DynamicFunction 766 | (static-combine (:op o) false) 767 | (doall (map #(static-combine % false) (:params o))) 768 | (:code o)) 769 | 770 | (instance? DynamicPath o) 771 | (->DynamicPath (static-combine (:path o))) 772 | 773 | (instance? DynamicVal o) 774 | o 775 | 776 | :else 777 | (if nav-pos? 778 | (coerce-nav o) 779 | o)))) 780 | 781 | 782 | #?(:cljs 783 | (do 784 | (defprotocol LateResolve 785 | (late-resolve [this dynamic-params])) 786 | 787 | ;; one of the "possible params" 788 | (defrecord LocalParam [idx] 789 | LateResolve 790 | (late-resolve [this dynamic-params] 791 | (nth dynamic-params idx))) 792 | 793 | ;; statically precomputed 794 | (defrecord StaticParam [val] 795 | LateResolve 796 | (late-resolve [this dynamic-params] 797 | val)) 798 | 799 | (mk-late-fn-records) 800 | (mk-late-fn))) 801 | 802 | #?(:clj 803 | (defn static-fn-code [afn args] 804 | `(~afn ~@args)) 805 | 806 | :cljs 807 | (defn static-fn-code [afn args] 808 | (late-fn (->StaticParam afn) args))) 809 | 810 | #?(:clj 811 | (defn dynamic-fn-code [afn args] 812 | `(~afn ~@args)) 813 | 814 | :cljs 815 | (defn dynamic-fn-code [afn args] 816 | (late-fn afn args))) 817 | 818 | #?(:clj 819 | (defn dynamic-val-code [code possible-params] 820 | code) 821 | 822 | :cljs 823 | (defn dynamic-val-code [code possible-params] 824 | (let [[i] (keep-indexed (fn [i v] (if (= v code) i)) possible-params)] 825 | (if (nil? i) 826 | (throw (ex-info "Could not find code in possible params" 827 | {:code code :possible-params possible-params}))) 828 | (maybe-direct-nav 829 | (->LocalParam i) 830 | (direct-nav? code))))) 831 | 832 | #?(:clj 833 | (defn static-val-code [o] 834 | o) 835 | 836 | :cljs 837 | (defn static-val-code [o] 838 | (maybe-direct-nav 839 | (->StaticParam o) 840 | (direct-nav? o)))) 841 | 842 | 843 | (declare resolve-nav-code) 844 | 845 | (defn dynamic->code [o] 846 | ;; works because both DynamicVal and DynamicFunction have :code field 847 | (walk-until dynamic-param? :code o)) 848 | 849 | (defn resolve-arg-code [o possible-params] 850 | (cond (instance? DynamicFunction o) 851 | (let [op (resolve-arg-code (:op o) possible-params) 852 | params (map #(resolve-arg-code % possible-params) (:params o))] 853 | (maybe-direct-nav 854 | (dynamic-fn-code (original-obj op) params) 855 | (direct-nav? (:op o)))) 856 | 857 | (instance? DynamicVal o) 858 | (dynamic-val-code (:code o) possible-params) 859 | 860 | (instance? DynamicPath o) 861 | (resolve-nav-code o possible-params) 862 | 863 | :else 864 | ;; handle dynamic params nested inside data structures 865 | ;; e.g. (terminal-val [v]) 866 | (if (identical? NONE (walk-select dynamic-param? identity o)) 867 | (static-val-code o) 868 | ;; done this way so it's compatible with cljs as well (since this dynamic val will be 869 | ;; a possible param) 870 | (resolve-arg-code (->DynamicVal (dynamic->code o)) possible-params) 871 | ))) 872 | 873 | (defn resolve-nav-code [o possible-params] 874 | (cond 875 | (instance? DynamicPath o) 876 | (let [path (:path o)] 877 | (if (sequential? path) 878 | (let [resolved (vec (map #(resolve-nav-code % possible-params) path))] 879 | (cond (empty? resolved) (static-val-code STAY*) 880 | (= 1 (count resolved)) (first resolved) 881 | :else (static-fn-code comp-navs resolved))) 882 | (resolve-nav-code path possible-params))) 883 | 884 | (instance? DynamicVal o) 885 | (let [code (:code o) 886 | d (dynamic-val-code code possible-params)] 887 | (cond (direct-nav? code) 888 | d 889 | 890 | (or (set? code) (and (fn-invocation? code) (= 'fn* (first code)))) 891 | (static-fn-code pred* [d]) 892 | 893 | :else 894 | (static-fn-code coerce-nav [d]))) 895 | 896 | (instance? DynamicFunction o) 897 | (let [res (resolve-arg-code o possible-params)] 898 | (if (direct-nav? res) res (static-fn-code coerce-nav [res]))) 899 | 900 | :else 901 | (static-val-code (coerce-nav o)))) 902 | 903 | (defn used-locals [locals-set form] 904 | (let [used-locals-cell (mutable-cell [])] 905 | (walk/postwalk 906 | (fn [e] 907 | (if (contains? locals-set e) 908 | (update-cell! used-locals-cell #(conj % e)) 909 | e)) 910 | form) 911 | (get-cell used-locals-cell))) 912 | 913 | (def ^:dynamic *DEBUG-INLINE-CACHING* false) 914 | 915 | #?(:cljs 916 | (defn mk-fn-name-strs [o] 917 | (walk/postwalk 918 | (fn [e] 919 | (if (fn? e) (re-find #" .*" (pr-str e)) e)) 920 | o))) 921 | 922 | (def ^:dynamic *path-compile-files* false) 923 | 924 | #?(:clj 925 | (defn mk-dynamic-path-maker [resolved-code ns-str used-locals-list possible-param] 926 | (let [code `(fn [~@used-locals-list] ~resolved-code) 927 | ns (find-ns (symbol ns-str))] 928 | (when *DEBUG-INLINE-CACHING* 929 | (println "Produced code:") 930 | (pp/pprint code) 931 | (println)) 932 | (binding [*ns* ns 933 | *compile-files* (if *path-compile-files* 934 | *compile-files* 935 | false)] 936 | (eval+ code)))) 937 | 938 | :cljs 939 | (defn mk-dynamic-path-maker [resolved-code ns-str used-locals-list possible-params] 940 | (when *DEBUG-INLINE-CACHING* 941 | (println "Possible params:") 942 | (println possible-params) 943 | (println "\nProduced dynamic object:") 944 | (println (mk-fn-name-strs resolved-code)) 945 | (println)) 946 | (fn [dynamic-params] 947 | (late-resolve resolved-code dynamic-params)))) 948 | 949 | 950 | ;; TODO: could have a global flag about whether or not to compile and cache static 951 | ;; portions, or whether to compile everything together on each invocation (so that 952 | ;; it can be redefined in repl 953 | ;; could have three levels: 954 | ;; 1. NO-COERCION (never allow coerce-nav at runtime) 955 | ;; 2. REGULAR (allow coerce-nav at runtime, cache static parts together) 956 | ;; 3. REDEFINABLE-VARS (don't cache static parts together) 957 | (defn magic-precompilation [path ns-str used-locals-list possible-params] 958 | (let [magic-path (-> path magic-precompilation* static-combine)] 959 | (when *DEBUG-INLINE-CACHING* 960 | (println "Inline caching debug information") 961 | (println "--------------------------------") 962 | (println "Input path:" path "\n") 963 | (println "Processed path:" magic-path "\n")) 964 | (if (rich-nav? magic-path) 965 | (do 966 | (when *DEBUG-INLINE-CACHING* 967 | (println "Static result:" magic-path)) 968 | (->CachedPathInfo false magic-path)) 969 | (let [maker (mk-dynamic-path-maker 970 | (resolve-nav-code (->DynamicPath magic-path) possible-params) 971 | ns-str 972 | used-locals-list 973 | possible-params)] 974 | (->CachedPathInfo true maker))))) 975 | 976 | 977 | 978 | (defn compiled-setval* [path val structure] 979 | (compiled-transform* path (fast-constantly val) structure)) 980 | 981 | (defn compiled-replace-in* 982 | [path transform-fn structure & {:keys [merge-fn] :or {merge-fn concat}}] 983 | (let [state (mutable-cell nil)] 984 | [(compiled-transform* path 985 | (fn [& args] 986 | (let [res (apply transform-fn args)] 987 | (if res 988 | (let [[ret user-ret] res] 989 | (->> user-ret 990 | (merge-fn (get-cell state)) 991 | (set-cell! state)) 992 | ret) 993 | (last args)))) 994 | structure) 995 | (get-cell state)])) 996 | 997 | 998 | (defn- multi-transform-error-fn [& nav] 999 | (throw 1000 | (ex-info "All navigation in multi-transform must end in 'terminal' navigators" 1001 | {:nav nav}))) 1002 | 1003 | (defn compiled-multi-transform* [path structure] 1004 | (compiled-transform* path multi-transform-error-fn structure)) 1005 | -------------------------------------------------------------------------------- /src/clj/com/rpl/specter/macros.clj: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.macros 2 | (:use [com.rpl.specter.protocols :only [RichNavigator]]) 3 | (:require [com.rpl.specter.impl :as i])) 4 | 5 | 6 | (defn- determine-params-impls [impls] 7 | (let [grouped (->> impls (map (fn [[n & body]] [n body])) (into {}))] 8 | (if-not (= #{'select* 'transform*} (-> grouped keys set)) 9 | (throw (ex-info "defnav must implement select* and transform*" 10 | {:methods (keys grouped)}))) 11 | grouped)) 12 | 13 | 14 | (defmacro richnav [params & impls] 15 | (if (empty? params) 16 | `(reify RichNavigator ~@impls) 17 | `(i/direct-nav-obj 18 | (fn ~params 19 | (reify RichNavigator 20 | ~@impls))))) 21 | 22 | 23 | (defmacro nav [params & impls] 24 | (let [{[[_ s-structure-sym s-next-fn-sym] & s-body] 'select* 25 | [[_ t-structure-sym t-next-fn-sym] & t-body] 'transform*} (determine-params-impls impls)] 26 | `(richnav ~params 27 | (~'select* [this# vals# ~s-structure-sym next-fn#] 28 | (let [~s-next-fn-sym (fn [s#] (next-fn# vals# s#))] 29 | ~@s-body)) 30 | (~'transform* [this# vals# ~t-structure-sym next-fn#] 31 | (let [~t-next-fn-sym (fn [s#] (next-fn# vals# s#))] 32 | ~@t-body))))) 33 | 34 | (defn- helper-name [name method-name] 35 | (with-meta (symbol (str name "-" method-name)) {:no-doc true})) 36 | 37 | (defmacro defnav [name params & impls] 38 | ;; remove the "this" param for the helper 39 | (let [helpers (for [[mname [_ & mparams] & mbody] impls] 40 | `(defn ~(helper-name name mname) [~@params ~@mparams] ~@mbody)) 41 | decls (for [[mname & _] impls] 42 | `(declare ~(helper-name name mname))) 43 | name-with-meta (vary-meta name 44 | assoc :arglists (list 'quote (list params)))] 45 | `(do 46 | ~@decls 47 | ~@helpers 48 | (def ~name-with-meta (nav ~params ~@impls))))) 49 | 50 | (defmacro defrichnav [name params & impls] 51 | (let [name-with-meta (vary-meta name 52 | assoc :arglists (list 'quote (list params)))] 53 | `(def ~name-with-meta 54 | (richnav ~params ~@impls)))) 55 | -------------------------------------------------------------------------------- /src/clj/com/rpl/specter/navs.cljc: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.navs 2 | #?(:cljs (:require-macros 3 | [com.rpl.specter 4 | :refer 5 | [defnav defrichnav]] 6 | [com.rpl.specter.util-macros :refer 7 | [doseqres]])) 8 | #?(:clj (:use [com.rpl.specter.macros :only [defnav defrichnav]] 9 | [com.rpl.specter.util-macros :only [doseqres]])) 10 | (:require [com.rpl.specter.impl :as i] 11 | #?@(:bb [] 12 | :clj [[clojure.core.reducers :as r]]))) 13 | 14 | 15 | (defn not-selected?* 16 | [compiled-path vals structure] 17 | (->> structure 18 | (i/compiled-select-any* compiled-path vals) 19 | (identical? i/NONE))) 20 | 21 | (defn selected?* 22 | [compiled-path vals structure] 23 | (not (not-selected?* compiled-path vals structure))) 24 | 25 | 26 | (defn all-select [structure next-fn] 27 | (doseqres i/NONE [e structure] 28 | (next-fn e))) 29 | 30 | #?( 31 | :clj 32 | (defn queue? [coll] 33 | (instance? clojure.lang.PersistentQueue coll)) 34 | 35 | :cljs 36 | (defn queue? [coll] 37 | (= (type coll) (type #queue [])))) 38 | 39 | 40 | (defprotocol AllTransformProtocol 41 | (all-transform [structure next-fn])) 42 | 43 | (defn void-transformed-kv-pair? [newkv] 44 | (or (identical? newkv i/NONE) (< (count newkv) 2))) 45 | 46 | (defn- non-transient-map-all-transform [structure next-fn empty-map] 47 | (reduce-kv 48 | (fn [m k v] 49 | (let [newkv (next-fn [k v])] 50 | (if (void-transformed-kv-pair? newkv) 51 | m 52 | (assoc m (nth newkv 0) (nth newkv 1))))) 53 | 54 | empty-map 55 | structure)) 56 | 57 | (defn not-NONE? [v] 58 | (-> v (identical? i/NONE) not)) 59 | 60 | 61 | (defn- all-transform-list [structure next-fn] 62 | (doall (sequence (comp (map next-fn) (filter not-NONE?)) structure))) 63 | 64 | (defn- all-transform-record [structure next-fn] 65 | (reduce 66 | (fn [res kv] (conj res (next-fn kv))) 67 | structure 68 | structure 69 | )) 70 | 71 | (extend-protocol AllTransformProtocol 72 | nil 73 | (all-transform [structure next-fn] 74 | nil) 75 | 76 | 77 | #?(:clj clojure.lang.MapEntry) 78 | #?(:clj 79 | (all-transform [structure next-fn] 80 | (let [newk (next-fn (key structure)) 81 | newv (next-fn (val structure))] 82 | (clojure.lang.MapEntry. newk newv)))) 83 | 84 | 85 | #?(:cljs cljs.core/MapEntry) 86 | #?(:cljs 87 | (all-transform [structure next-fn] 88 | (let [newk (next-fn (key structure)) 89 | newv (next-fn (val structure))] 90 | (cljs.core/->MapEntry newk newv nil)))) 91 | 92 | #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) 93 | (all-transform [structure next-fn] 94 | (into [] 95 | (comp (map next-fn) 96 | (filter not-NONE?)) 97 | structure)) 98 | 99 | #?(:clj String :cljs string) 100 | (all-transform [structure next-fn] 101 | (apply str (into [] 102 | (comp (map next-fn) 103 | (filter not-NONE?)) 104 | structure))) 105 | 106 | #?(:clj clojure.lang.PersistentHashSet :cljs cljs.core/PersistentHashSet) 107 | (all-transform [structure next-fn] 108 | (into #{} 109 | (comp (map next-fn) 110 | (filter not-NONE?)) 111 | structure)) 112 | 113 | #?(:clj clojure.lang.PersistentArrayMap) 114 | #?(:bb 115 | (all-transform [structure next-fn] 116 | (non-transient-map-all-transform structure next-fn {})) 117 | :clj 118 | (all-transform [structure next-fn] 119 | (let [k-it (.keyIterator structure) 120 | v-it (.valIterator structure) 121 | none-cell (i/mutable-cell 0) 122 | len (.count structure) 123 | array (i/fast-object-array (* 2 len))] 124 | (loop [i 0 125 | j 0] 126 | (if (.hasNext k-it) 127 | (let [k (.next k-it) 128 | v (.next v-it) 129 | newkv (next-fn [k v])] 130 | (if (void-transformed-kv-pair? newkv) 131 | (do 132 | (i/update-cell! none-cell inc) 133 | (recur (+ i 2) j)) 134 | (do 135 | (aset array j (nth newkv 0)) 136 | (aset array (inc j) (nth newkv 1)) 137 | (recur (+ i 2) (+ j 2))))))) 138 | (let [none-count (i/get-cell none-cell) 139 | array (if (not= 0 none-count) 140 | (java.util.Arrays/copyOf array (int (* 2 (- len none-count)))) 141 | array 142 | )] 143 | (clojure.lang.PersistentArrayMap/createAsIfByAssoc array))))) 144 | 145 | 146 | #?(:cljs cljs.core/PersistentArrayMap) 147 | #?(:cljs 148 | (all-transform [structure next-fn] 149 | (non-transient-map-all-transform structure next-fn {}))) 150 | 151 | 152 | #?(:clj clojure.lang.PersistentTreeMap :cljs cljs.core/PersistentTreeMap) 153 | (all-transform [structure next-fn] 154 | (non-transient-map-all-transform structure next-fn (empty structure))) 155 | 156 | #?(:clj clojure.lang.IRecord) 157 | #?(:clj 158 | (all-transform [structure next-fn] 159 | (all-transform-record structure next-fn))) 160 | 161 | #?(:clj clojure.lang.PersistentHashMap :cljs cljs.core/PersistentHashMap) 162 | (all-transform [structure next-fn] 163 | (persistent! 164 | (reduce-kv 165 | (fn [m k v] 166 | (let [newkv (next-fn [k v])] 167 | (if (void-transformed-kv-pair? newkv) 168 | m 169 | (assoc! m (nth newkv 0) (nth newkv 1))))) 170 | 171 | (transient 172 | #?(:clj clojure.lang.PersistentHashMap/EMPTY :cljs cljs.core.PersistentHashMap.EMPTY)) 173 | 174 | structure))) 175 | 176 | 177 | 178 | #?(:clj Object) 179 | #?(:clj 180 | (all-transform [structure next-fn] 181 | (let [empty-structure (empty structure)] 182 | (cond (and (list? empty-structure) (not (queue? empty-structure))) 183 | (all-transform-list structure next-fn) 184 | 185 | (map? structure) 186 | ;; reduce-kv is much faster than doing r/map through call to (into ...) 187 | (reduce-kv 188 | (fn [m k v] 189 | (let [newkv (next-fn [k v])] 190 | (if (void-transformed-kv-pair? newkv) 191 | m 192 | (assoc m (nth newkv 0) (nth newkv 1))))) 193 | 194 | empty-structure 195 | structure) 196 | 197 | 198 | :else 199 | #?(:bb (into empty-structure 200 | (comp (map next-fn) (filter not-NONE?)) 201 | structure) 202 | :clj (->> structure 203 | (r/map next-fn) 204 | (r/filter not-NONE?) 205 | (into empty-structure))))))) 206 | 207 | 208 | #?(:cljs default) 209 | #?(:cljs 210 | (all-transform [structure next-fn] 211 | (if (record? structure) 212 | ;; this case is solely for cljs since extending to IRecord doesn't work for cljs 213 | (all-transform-record structure next-fn) 214 | (let [empty-structure (empty structure)] 215 | (cond 216 | (and (list? empty-structure) (not (queue? empty-structure))) 217 | (all-transform-list structure next-fn) 218 | 219 | (map? structure) 220 | (reduce-kv 221 | (fn [m k v] 222 | (let [newkv (next-fn [k v])] 223 | (if (void-transformed-kv-pair? newkv) 224 | m 225 | (assoc m (nth newkv 0) (nth newkv 1))))) 226 | empty-structure 227 | structure) 228 | 229 | :else 230 | (into empty-structure 231 | (comp (map next-fn) (filter not-NONE?)) 232 | structure))))))) 233 | 234 | 235 | 236 | (defprotocol MapTransformProtocol 237 | (map-vals-transform [structure next-fn]) 238 | (map-keys-transform [structure next-fn]) 239 | ) 240 | 241 | 242 | 243 | (defn map-vals-non-transient-transform [structure empty-map next-fn] 244 | (reduce-kv 245 | (fn [m k v] 246 | (let [newv (next-fn v)] 247 | (if (identical? newv i/NONE) 248 | m 249 | (assoc m k newv)))) 250 | empty-map 251 | structure)) 252 | 253 | (defn map-keys-non-transient-transform [structure empty-map next-fn] 254 | (reduce-kv 255 | (fn [m k v] 256 | (let [newk (next-fn k)] 257 | (if (identical? newk i/NONE) 258 | m 259 | (assoc m newk v)))) 260 | empty-map 261 | structure)) 262 | 263 | (extend-protocol MapTransformProtocol 264 | nil 265 | (map-vals-transform [structure next-fn] 266 | nil) 267 | (map-keys-transform [structure next-fn] 268 | nil) 269 | 270 | 271 | #?(:clj clojure.lang.PersistentArrayMap) 272 | #?(:bb 273 | (map-vals-transform [structure next-fn] 274 | (map-vals-non-transient-transform structure {} next-fn)) 275 | :clj 276 | (map-vals-transform [structure next-fn] 277 | (let [k-it (.keyIterator structure) 278 | v-it (.valIterator structure) 279 | none-cell (i/mutable-cell 0) 280 | len (.count structure) 281 | array (i/fast-object-array (* 2 len))] 282 | (loop [i 0 283 | j 0] 284 | (if (.hasNext k-it) 285 | (let [k (.next k-it) 286 | v (.next v-it) 287 | newv (next-fn v)] 288 | (if (identical? newv i/NONE) 289 | (do 290 | (i/update-cell! none-cell inc) 291 | (recur (+ i 2) j)) 292 | (do 293 | (aset array j k) 294 | (aset array (inc j) newv) 295 | (recur (+ i 2) (+ j 2))))))) 296 | (let [none-count (i/get-cell none-cell) 297 | array (if (not= 0 none-count) 298 | (java.util.Arrays/copyOf array (int (* 2 (- len none-count)))) 299 | array 300 | )] 301 | (clojure.lang.PersistentArrayMap. array))))) 302 | #?(:bb 303 | (map-keys-transform [structure next-fn] 304 | (map-keys-non-transient-transform structure {} next-fn)) 305 | :clj 306 | (map-keys-transform [structure next-fn] 307 | (let [k-it (.keyIterator structure) 308 | v-it (.valIterator structure) 309 | none-cell (i/mutable-cell 0) 310 | len (.count structure) 311 | array (i/fast-object-array (* 2 len))] 312 | (loop [i 0 313 | j 0] 314 | (if (.hasNext k-it) 315 | (let [k (.next k-it) 316 | v (.next v-it) 317 | newk (next-fn k)] 318 | (if (identical? newk i/NONE) 319 | (do 320 | (i/update-cell! none-cell inc) 321 | (recur (+ i 2) j)) 322 | (do 323 | (aset array j newk) 324 | (aset array (inc j) v) 325 | (recur (+ i 2) (+ j 2))))))) 326 | (let [none-count (i/get-cell none-cell) 327 | array (if (not= 0 none-count) 328 | (java.util.Arrays/copyOf array (int (* 2 (- len none-count)))) 329 | array 330 | )] 331 | (clojure.lang.PersistentArrayMap/createAsIfByAssoc array))))) 332 | 333 | #?(:cljs cljs.core/PersistentArrayMap) 334 | #?(:cljs 335 | (map-vals-transform [structure next-fn] 336 | (map-vals-non-transient-transform structure {} next-fn))) 337 | #?(:cljs 338 | (map-keys-transform [structure next-fn] 339 | (map-keys-non-transient-transform structure {} next-fn))) 340 | 341 | 342 | #?(:clj clojure.lang.PersistentTreeMap :cljs cljs.core/PersistentTreeMap) 343 | (map-vals-transform [structure next-fn] 344 | (map-vals-non-transient-transform structure (empty structure) next-fn)) 345 | (map-keys-transform [structure next-fn] 346 | (map-keys-non-transient-transform structure (empty structure) next-fn)) 347 | 348 | 349 | #?(:clj clojure.lang.PersistentHashMap :cljs cljs.core/PersistentHashMap) 350 | (map-vals-transform [structure next-fn] 351 | (persistent! 352 | (reduce-kv 353 | (fn [m k v] 354 | (let [newv (next-fn v)] 355 | (if (identical? newv i/NONE) 356 | m 357 | (assoc! m k newv)))) 358 | (transient 359 | #?(:clj clojure.lang.PersistentHashMap/EMPTY :cljs cljs.core.PersistentHashMap.EMPTY)) 360 | 361 | structure))) 362 | (map-keys-transform [structure next-fn] 363 | (persistent! 364 | (reduce-kv 365 | (fn [m k v] 366 | (let [newk (next-fn k)] 367 | (if (identical? newk i/NONE) 368 | m 369 | (assoc! m newk v)))) 370 | (transient 371 | #?(:clj clojure.lang.PersistentHashMap/EMPTY :cljs cljs.core.PersistentHashMap.EMPTY)) 372 | 373 | structure))) 374 | 375 | #?(:clj Object :cljs default) 376 | (map-vals-transform [structure next-fn] 377 | (reduce-kv 378 | (fn [m k v] 379 | (let [newv (next-fn v)] 380 | (if (identical? newv i/NONE) 381 | m 382 | (assoc m k newv)))) 383 | (empty structure) 384 | structure)) 385 | (map-keys-transform [structure next-fn] 386 | (reduce-kv 387 | (fn [m k v] 388 | (let [newk (next-fn k)] 389 | (if (identical? newk i/NONE) 390 | m 391 | (assoc m newk v)))) 392 | (empty structure) 393 | structure))) 394 | 395 | (defn srange-select [structure start end next-fn] 396 | (next-fn 397 | (if (string? structure) 398 | (subs structure start end) 399 | (-> structure vec (subvec start end)) 400 | ))) 401 | 402 | (def srange-transform i/srange-transform*) 403 | 404 | 405 | (defn extract-basic-filter-fn [path] 406 | (cond (fn? path) 407 | path 408 | 409 | (and (coll? path) 410 | (every? fn? path)) 411 | (reduce 412 | (fn [combined afn] 413 | (fn [structure] 414 | (and (combined structure) (afn structure)))) 415 | 416 | path))) 417 | 418 | 419 | 420 | 421 | (defn if-select [vals structure next-fn then-tester then-nav else-nav] 422 | (i/exec-select* 423 | (if (then-tester structure) then-nav else-nav) 424 | vals 425 | structure 426 | next-fn)) 427 | 428 | 429 | 430 | (defn if-transform [vals structure next-fn then-tester then-nav else-nav] 431 | (i/exec-transform* 432 | (if (then-tester structure) then-nav else-nav) 433 | vals 434 | structure 435 | next-fn)) 436 | 437 | 438 | 439 | 440 | (defprotocol AddExtremes 441 | (append-all [structure elements]) 442 | (prepend-all [structure elements]) 443 | (append-one [structure elem]) 444 | (prepend-one [structure elem]) 445 | ) 446 | 447 | (extend-protocol AddExtremes 448 | nil 449 | (append-all [_ elements] 450 | elements) 451 | (prepend-all [_ elements] 452 | elements) 453 | (append-one [_ elem] 454 | (list elem)) 455 | (prepend-one [_ elem] 456 | (list elem)) 457 | 458 | #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) 459 | (append-all [structure elements] 460 | (reduce conj structure elements)) 461 | (prepend-all [structure elements] 462 | (let [ret (transient [])] 463 | (as-> ret <> 464 | (reduce conj! <> elements) 465 | (reduce conj! <> structure) 466 | (persistent! <>)))) 467 | (append-one [structure elem] 468 | (conj structure elem)) 469 | (prepend-one [structure elem] 470 | (into [elem] structure)) 471 | 472 | #?(:cljs cljs.core/Subvec) 473 | #?(:cljs 474 | (append-all [structure elements] 475 | (reduce conj structure elements))) 476 | #?(:cljs 477 | (prepend-all [structure elements] 478 | (let [ret (transient [])] 479 | (as-> ret <> 480 | (reduce conj! <> elements) 481 | (reduce conj! <> structure) 482 | (persistent! <>))))) 483 | #?(:cljs 484 | (append-one [structure elem] 485 | (conj structure elem))) 486 | #?(:cljs 487 | (prepend-one [structure elem] 488 | (into [elem] structure))) 489 | 490 | 491 | #?(:clj Object :cljs default) 492 | (append-all [structure elements] 493 | (concat structure elements)) 494 | (prepend-all [structure elements] 495 | (concat elements structure)) 496 | (append-one [structure elem] 497 | (concat structure [elem])) 498 | (prepend-one [structure elem] 499 | (cons elem structure)) 500 | ) 501 | 502 | 503 | 504 | (defprotocol UpdateExtremes 505 | (update-first [s afn]) 506 | (update-last [s afn])) 507 | 508 | (defprotocol GetExtremes 509 | (get-first [s]) 510 | (get-last [s])) 511 | 512 | (defprotocol FastEmpty 513 | (fast-empty? [s])) 514 | 515 | (defprotocol InsertBeforeIndex 516 | (insert-before-idx [aseq idx val])) 517 | 518 | (defnav PosNavigator [getter updater] 519 | (select* [this structure next-fn] 520 | (if-not (fast-empty? structure) 521 | (next-fn (getter structure)) 522 | i/NONE)) 523 | (transform* [this structure next-fn] 524 | (if (fast-empty? structure) 525 | structure 526 | (updater structure next-fn)))) 527 | 528 | #?(:bb 529 | (defn vec-count [v] 530 | (count v)) 531 | 532 | :clj 533 | (defn vec-count [^clojure.lang.IPersistentVector v] 534 | (.length v)) 535 | 536 | :cljs 537 | (defn vec-count [v] 538 | (count v))) 539 | 540 | (defn- update-first-list [l afn] 541 | (let [newf (afn (first l)) 542 | restl (rest l)] 543 | (if (identical? i/NONE newf) 544 | restl 545 | (cons newf restl)))) 546 | 547 | (defn- update-last-list [l afn] 548 | (let [lastl (afn (last l)) 549 | bl (butlast l)] 550 | (if (identical? i/NONE lastl) 551 | (if (nil? bl) '() bl) 552 | (concat bl [lastl])))) 553 | 554 | (defn- update-first-vector [v afn] 555 | (let [val (nth v 0) 556 | newv (afn val)] 557 | (if (identical? i/NONE newv) 558 | (subvec v 1) 559 | (assoc v 0 newv) 560 | ))) 561 | 562 | (defn- update-last-vector [v afn] 563 | ;; type-hinting vec-count to ^int caused weird errors with case 564 | (let [c (int (vec-count v))] 565 | (case c 566 | 1 (let [[e] v 567 | newe (afn e)] 568 | (if (identical? i/NONE newe) 569 | [] 570 | [newe])) 571 | 2 (let [[e1 e2] v 572 | newe (afn e2)] 573 | (if (identical? i/NONE newe) 574 | [e1] 575 | [e1 newe])) 576 | (let [i (dec c) 577 | newe (afn (nth v i))] 578 | (if (identical? i/NONE newe) 579 | (pop v) 580 | (assoc v i newe)))))) 581 | 582 | 583 | #?(:bb 584 | (defn transient-vec-count [v] 585 | (count v)) 586 | 587 | :clj 588 | (defn transient-vec-count [^clojure.lang.ITransientVector v] 589 | (.count v)) 590 | 591 | :cljs 592 | (defn transient-vec-count [v] 593 | (count v))) 594 | 595 | 596 | (extend-protocol UpdateExtremes 597 | #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) 598 | (update-first [v afn] 599 | (update-first-vector v afn)) 600 | 601 | (update-last [v afn] 602 | (update-last-vector v afn)) 603 | 604 | #?(:cljs cljs.core/Subvec) 605 | #?(:cljs 606 | (update-first [v afn] 607 | (update-first-vector v afn))) 608 | #?(:cljs 609 | (update-last [v afn] 610 | (update-last-vector v afn))) 611 | 612 | #?(:clj String :cljs string) 613 | (update-first [s afn] 614 | (let [rests (subs s 1 (count s)) 615 | newb (afn (nth s 0))] 616 | (if (identical? i/NONE newb) 617 | rests 618 | (str newb rests)))) 619 | 620 | (update-last [s afn] 621 | (let [last-idx (-> s count dec) 622 | newl (afn (nth s last-idx)) 623 | begins (subs s 0 last-idx)] 624 | (if (identical? i/NONE newl) 625 | begins 626 | (str begins newl) 627 | ))) 628 | 629 | #?(:cljs cljs.core/MapEntry) 630 | #?(:cljs 631 | (update-first [e afn] 632 | (cljs.core/->MapEntry (-> e key afn) (val e) nil))) 633 | #?(:cljs 634 | (update-last [e afn] 635 | (cljs.core/->MapEntry (key e) (-> e val afn) nil))) 636 | 637 | #?(:clj Object :cljs default) 638 | (update-first [l val] 639 | (update-first-list l val)) 640 | (update-last [l val] 641 | (update-last-list l val))) 642 | 643 | 644 | (extend-protocol GetExtremes 645 | #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) 646 | (get-first [v] 647 | (nth v 0)) 648 | (get-last [v] 649 | (peek v)) 650 | 651 | #?(:clj Object :cljs default) 652 | (get-first [s] 653 | (first s)) 654 | (get-last [s] 655 | (last s)) 656 | 657 | #?(:cljs cljs.core/MapEntry) 658 | #?(:cljs 659 | (get-first [e] 660 | (key e))) 661 | #?(:cljs 662 | (get-last [e] 663 | (val e))) 664 | 665 | #?(:clj String :cljs string) 666 | (get-first [s] 667 | (nth s 0)) 668 | (get-last [s] 669 | (nth s (-> s count dec)) 670 | )) 671 | 672 | 673 | 674 | (extend-protocol FastEmpty 675 | nil 676 | (fast-empty? [_] true) 677 | 678 | #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) 679 | (fast-empty? [v] 680 | (= 0 (vec-count v))) 681 | #?(:clj clojure.lang.ITransientVector :cljs cljs.core/TransientVector) 682 | (fast-empty? [v] 683 | (= 0 (transient-vec-count v))) 684 | #?(:clj Object :cljs default) 685 | (fast-empty? [s] 686 | (empty? s))) 687 | 688 | 689 | (defn- do-keypath-transform [vals structure key next-fn] 690 | (let [newv (next-fn vals (get structure key))] 691 | (if (identical? newv i/NONE) 692 | (if (sequential? structure) 693 | (i/srange-transform* structure key (inc key) (fn [_] [])) 694 | (dissoc structure key)) 695 | (assoc structure key newv)))) 696 | 697 | (defrichnav 698 | ^{:doc "Navigates to the specified key, navigating to nil if it does not exist. 699 | Setting the value to NONE will remove it from the collection."} 700 | keypath* 701 | [key] 702 | (select* [this vals structure next-fn] 703 | (next-fn vals (get structure key))) 704 | (transform* [this vals structure next-fn] 705 | (do-keypath-transform vals structure key next-fn) 706 | )) 707 | 708 | 709 | (defrichnav 710 | ^{:doc "Navigates to the key only if it exists in the map. Setting the value to NONE 711 | will remove it from the collection."} 712 | must* 713 | [k] 714 | (select* [this vals structure next-fn] 715 | (if (contains? structure k) 716 | (next-fn vals (get structure k)) 717 | i/NONE)) 718 | (transform* [this vals structure next-fn] 719 | (if (contains? structure k) 720 | (do-keypath-transform vals structure k next-fn) 721 | structure))) 722 | 723 | (defrichnav nthpath* 724 | ^{:doc "Navigates to the given position in the sequence. Setting the value to NONE 725 | will remove it from the sequence. Works for all sequence types."} 726 | [i] 727 | (select* [this vals structure next-fn] 728 | (next-fn vals (nth structure i))) 729 | (transform* [this vals structure next-fn] 730 | (if (vector? structure) 731 | (let [newv (next-fn vals (nth structure i))] 732 | (if (identical? newv i/NONE) 733 | (i/srange-transform* structure i (inc i) (fn [_] [])) 734 | (assoc structure i newv))) 735 | (i/srange-transform* ; can make this much more efficient with alternate impl 736 | structure 737 | i 738 | (inc i) 739 | (fn [[e]] 740 | (let [v (next-fn vals e)] 741 | (if (identical? v i/NONE) 742 | [] 743 | [v]) 744 | )))))) 745 | 746 | (defrecord SrangeEndFunction [end-fn]) 747 | 748 | ;; done this way to maintain backwards compatibility 749 | (defn invoke-end-fn [end-fn structure start] 750 | (if (instance? SrangeEndFunction end-fn) 751 | ((:end-fn end-fn) structure start) 752 | (end-fn structure) 753 | )) 754 | 755 | (defn- insert-before-index-list [lst idx val] 756 | ;; an implementation that is most efficient for list style structures 757 | (let [[front back] (split-at idx lst)] 758 | (concat front (cons val back)))) 759 | 760 | (extend-protocol InsertBeforeIndex 761 | nil 762 | (insert-before-idx [_ idx val] 763 | (if (= 0 idx) 764 | (list val) 765 | (throw (ex-info "For a nil structure, can only insert before index 0" 766 | {:insertion-index idx})))) 767 | 768 | #?(:clj java.lang.String :cljs string) 769 | (insert-before-idx [aseq idx val] 770 | (apply str (insert-before-index-list aseq idx val))) 771 | 772 | #?(:clj clojure.lang.LazySeq :cljs cljs.core/LazySeq) 773 | (insert-before-idx [aseq idx val] 774 | (insert-before-index-list aseq idx val)) 775 | 776 | #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) 777 | (insert-before-idx [aseq idx val] 778 | (let [front (subvec aseq 0 idx) 779 | back (subvec aseq idx)] 780 | (into (conj front val) back))) 781 | 782 | #?(:clj clojure.lang.IPersistentList :cljs cljs.core/List) 783 | (insert-before-idx [aseq idx val] 784 | (cond (= idx 0) 785 | (cons val aseq) 786 | :else (insert-before-index-list aseq idx val)))) 787 | -------------------------------------------------------------------------------- /src/clj/com/rpl/specter/protocols.cljc: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.protocols) 2 | 3 | (defprotocol RichNavigator 4 | "Do not use this protocol directly. All navigators must be created using macros 5 | in com.rpl.specter namespace." 6 | (select* [this vals structure next-fn] 7 | "An implementation of `select*` must call `next-fn` on each 8 | subvalue of `structure`. The result of `select*` is specified 9 | as follows: 10 | 11 | 1. `NONE` if `next-fn` never called 12 | 2. `NONE` if all calls to `next-fn` return `NONE` 13 | 3. Otherwise, any non-`NONE` return value from calling `next-fn` 14 | ") 15 | (transform* [this vals structure next-fn] 16 | "An implementation of `transform*` must use `next-fn` to transform 17 | any subvalues of `structure` and then merge those transformed values 18 | back into `structure`. Everything else in `structure` must be unchanged.")) 19 | 20 | 21 | (defprotocol Collector 22 | "Do not use this protocol directly. All navigators must be created using 23 | macros in com.rpl.specter namespace." 24 | (collect-val [this structure])) 25 | 26 | (defprotocol ImplicitNav 27 | (implicit-nav [obj])) 28 | -------------------------------------------------------------------------------- /src/clj/com/rpl/specter/transients.cljc: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.transients 2 | #?(:cljs 3 | (:require-macros [com.rpl.specter 4 | :refer 5 | [defnav]])) 6 | (:use #?(:clj 7 | [com.rpl.specter :only 8 | [defnav]])) 9 | (:require [com.rpl.specter.navs :as n] 10 | [com.rpl.specter :refer [subselect selected?]])) 11 | 12 | (defnav 13 | ^{:doc "Navigates to the specified key of a transient collection, 14 | navigating to nil if it doesn't exist."} 15 | keypath! 16 | [key] 17 | (select* [this structure next-fn] 18 | (next-fn (get structure key))) 19 | (transform* [this structure next-fn] 20 | (assoc! structure key (next-fn (get structure key))))) 21 | 22 | 23 | 24 | (defnav 25 | ^{:doc "Navigates to an empty (persistent) vector at the end of a transient vector."} 26 | END! 27 | [] 28 | (select* [this structure next-fn] 29 | (next-fn [])) 30 | (transform* [this structure next-fn] 31 | (let [res (next-fn [])] 32 | (reduce conj! structure res)))) 33 | 34 | (defn- t-get-first 35 | [tv] 36 | (nth tv 0)) 37 | 38 | (defn- t-get-last 39 | [tv] 40 | (nth tv (dec (n/transient-vec-count tv)))) 41 | 42 | (defn- t-update-first 43 | [tv next-fn] 44 | (assoc! tv 0 (next-fn (nth tv 0)))) 45 | 46 | (defn- t-update-last 47 | [tv next-fn] 48 | (let [i (dec (n/transient-vec-count tv))] 49 | (assoc! tv i (next-fn (nth tv i))))) 50 | 51 | 52 | (def FIRST! 53 | "Navigates to the first element of a transient vector." 54 | (n/PosNavigator t-get-first t-update-first)) 55 | 56 | (def LAST! 57 | "Navigates to the last element of a transient vector." 58 | (n/PosNavigator t-get-last t-update-last)) 59 | 60 | #?( 61 | :clj 62 | (defn- select-keys-from-transient-map 63 | "Selects keys from transient map, because built-in select-keys uses 64 | `find` which is unsupported." 65 | [m m-keys] 66 | (loop [result {} 67 | m-keys m-keys] 68 | (if-not (seq m-keys) 69 | result 70 | (let [k (first m-keys) 71 | ;; support Clojure 1.6 where contains? is broken on transients 72 | item (get m k ::not-found)] 73 | (recur (if-not (identical? item ::not-found) 74 | (assoc result k item) 75 | result) 76 | (rest m-keys)))))) 77 | 78 | :cljs 79 | (defn- select-keys-from-transient-map 80 | "Uses select-keys on a transient map." 81 | [m m-keys] 82 | (select-keys m m-keys))) 83 | 84 | 85 | (defnav 86 | ^{:doc "Navigates to the specified persistent submap of a transient map."} 87 | submap! 88 | [m-keys] 89 | (select* [this structure next-fn] 90 | (next-fn (select-keys-from-transient-map structure m-keys))) 91 | (transform* [this structure next-fn] 92 | (let [selected (select-keys-from-transient-map structure m-keys) 93 | res (next-fn selected)] 94 | (as-> structure % 95 | (reduce (fn [m k] 96 | (dissoc! m k)) 97 | % m-keys) 98 | (reduce-kv (fn [m k v] 99 | (assoc! m k v)) 100 | % res))))) 101 | -------------------------------------------------------------------------------- /src/clj/com/rpl/specter/util_macros.clj: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.util-macros) 2 | 3 | (defmacro doseqres [backup-res [n aseq] & body] 4 | `(reduce 5 | (fn [curr# ~n] 6 | (let [ret# (do ~@body)] 7 | (if (identical? ret# ~backup-res) 8 | curr# 9 | (if (reduced? ret#) (reduced ret#) ret#)))) 10 | 11 | ~backup-res 12 | ~aseq)) 13 | 14 | (defn- gensyms [amt] 15 | (vec (repeatedly amt gensym))) 16 | 17 | (defmacro mk-comp-navs [] 18 | (let [impls (for [i (range 3 20)] 19 | (let [[fsym & rsyms :as syms] (gensyms i)] 20 | `([~@syms] (~'comp-navs ~fsym (~'comp-navs ~@rsyms))))) 21 | last-syms (gensyms 19)] 22 | `(defn ~'comp-navs 23 | ([] ~'com.rpl.specter.impl/STAY*) 24 | ([nav1#] nav1#) 25 | ([nav1# nav2#] (~'com.rpl.specter.impl/combine-two-navs nav1# nav2#)) 26 | ~@impls 27 | ([~@last-syms ~'& rest#] 28 | (~'comp-navs 29 | (~'comp-navs ~@last-syms) 30 | (reduce ~'comp-navs rest#)))))) 31 | 32 | 33 | 34 | ;;TODO: move these definitions somewhere else 35 | (defn late-fn-record-name [i] 36 | (symbol (str "LateFn" i))) 37 | 38 | (defn late-fn-record-constructor-name [i] 39 | (symbol (str "->LateFn" i))) 40 | 41 | (defn- mk-late-fn-record [i] 42 | (let [fields (concat ['fn] (for [j (range i)] (symbol (str "arg" j)))) 43 | dparams (gensym "dynamic-params") 44 | resolvers (for [f fields] 45 | `(~'late-resolve ~f ~dparams))] 46 | `(defrecord ~(late-fn-record-name i) [~@fields] 47 | ~'LateResolve 48 | (~'late-resolve [this# ~dparams] 49 | (~@resolvers))))) 50 | 51 | 52 | (defmacro mk-late-fn-records [] 53 | (let [impls (for [i (range 20)] (mk-late-fn-record i))] 54 | `(do ~@impls))) 55 | 56 | (defmacro mk-late-fn [] 57 | (let [f (gensym "afn") 58 | args (gensym "args") 59 | cases (for [i (range 19)] 60 | [i 61 | (let [gets (for [j (range i)] `(nth ~args ~j))] 62 | `(~(late-fn-record-constructor-name i) 63 | ~f 64 | ~@gets))])] 65 | `(defn ~'late-fn [~f ~args] 66 | (case (count ~args) 67 | ~@(apply concat cases) 68 | (throw (ex-info "Cannot have late function with more than 18 args" {})))))) 69 | -------------------------------------------------------------------------------- /src/clj/com/rpl/specter/zipper.cljc: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.zipper 2 | #?(:cljs (:require-macros 3 | [com.rpl.specter 4 | :refer [defnav nav declarepath providepath recursive-path]])) 5 | #?(:clj 6 | (:use 7 | [com.rpl.specter :only [defnav nav declarepath providepath 8 | recursive-path]])) 9 | (:require [com.rpl.specter :as s] 10 | [clojure.zip :as zip])) 11 | 12 | (defnav zipper [constructor] 13 | (select* [this structure next-fn] 14 | (next-fn (constructor structure))) 15 | (transform* [this structure next-fn] 16 | (zip/root (next-fn (constructor structure))))) 17 | 18 | 19 | (def VECTOR-ZIP (zipper zip/vector-zip)) 20 | (def SEQ-ZIP (zipper zip/seq-zip)) 21 | (def XML-ZIP (zipper zip/xml-zip)) 22 | 23 | 24 | (def ^{:doc "Navigate to the next element in the structure. 25 | If no next element, works like STOP."} 26 | NEXT 27 | (s/comp-paths 28 | (s/view zip/next) 29 | (s/if-path zip/end? 30 | s/STOP 31 | s/STAY))) 32 | 33 | 34 | (defn- mk-zip-nav [znav] 35 | (nav [] 36 | (select* [this structure next-fn] 37 | (let [ret (znav structure)] 38 | (if ret (next-fn ret)))) 39 | 40 | (transform* [this structure next-fn] 41 | (let [ret (znav structure)] 42 | (if ret (next-fn ret) structure))))) 43 | 44 | 45 | ;; (multi-path RIGHT LEFT) will not navigate to the right and left 46 | ;; of the currently navigated element because locations aren't stable 47 | ;; like they are for maps/graphs. The path following RIGHT could 48 | ;; insert lots of elements all over the sequence, and there's no 49 | ;; way to determine how to get "back". 50 | (def ^{:doc "Navigate to the element to the right. 51 | If no element there, works like STOP."} 52 | RIGHT (mk-zip-nav zip/right)) 53 | 54 | (def ^{:doc "Navigate to the element to the left. 55 | If no element there, works like STOP."} 56 | LEFT (mk-zip-nav zip/left)) 57 | 58 | (def DOWN (mk-zip-nav zip/down)) 59 | (def UP (mk-zip-nav zip/up)) 60 | 61 | (def ^{:doc "Navigate to the previous element. 62 | If this is the first element, works like STOP."} 63 | PREV (mk-zip-nav zip/prev)) 64 | 65 | (def RIGHTMOST (s/view zip/rightmost)) 66 | (def LEFTMOST (s/view zip/leftmost)) 67 | 68 | (defn- inner-insert [structure next-fn inserter mover backer] 69 | (let [to-insert (next-fn []) 70 | inserts (reduce 71 | (fn [z e] (-> z (inserter e) mover)) 72 | structure 73 | to-insert)] 74 | 75 | (if backer 76 | (reduce (fn [z _] (backer z)) inserts to-insert) 77 | inserts))) 78 | 79 | 80 | (defnav ^{:doc "Navigate to the empty subsequence directly to the 81 | right of this element."} 82 | INNER-RIGHT [] 83 | (select* [this structure next-fn] 84 | (next-fn [])) 85 | (transform* [this structure next-fn] 86 | (inner-insert structure next-fn zip/insert-right zip/right zip/left))) 87 | 88 | 89 | (defnav ^{:doc "Navigate to the empty subsequence directly to the 90 | left of this element."} 91 | INNER-LEFT [] 92 | (select* [this structure next-fn] 93 | (next-fn [])) 94 | (transform* [this structure next-fn] 95 | (inner-insert structure next-fn zip/insert-left identity nil))) 96 | 97 | 98 | (defnav NODE [] 99 | (select* [this structure next-fn] 100 | (next-fn (zip/node structure))) 101 | 102 | (transform* [this structure next-fn] 103 | (zip/edit structure next-fn))) 104 | 105 | 106 | (defnav ^{:doc "Navigate to the subsequence containing only 107 | the node currently pointed to. This works just 108 | like srange and can be used to remove elements 109 | from the structure"} 110 | NODE-SEQ [] 111 | (select* [this structure next-fn] 112 | (next-fn [(zip/node structure)])) 113 | 114 | (transform* [this structure next-fn] 115 | (let [to-insert (next-fn [(zip/node structure)]) 116 | inserted (reduce zip/insert-left structure to-insert)] 117 | (zip/remove inserted)))) 118 | 119 | 120 | (def ^{:doc "Navigate the zipper to the first element 121 | in the structure matching predfn. A linear scan 122 | is done using NEXT to find the element."} 123 | find-first 124 | (recursive-path [predfn] p 125 | (s/if-path [NODE (s/pred predfn)] 126 | s/STAY 127 | [NEXT p]))) 128 | 129 | 130 | 131 | (declarepath ^{:doc "Navigate to every element reachable using calls 132 | to NEXT"} 133 | NEXT-WALK) 134 | 135 | (providepath NEXT-WALK 136 | (s/stay-then-continue 137 | NEXT 138 | NEXT-WALK)) 139 | -------------------------------------------------------------------------------- /src/java/com/rpl/specter/MutableCell.java: -------------------------------------------------------------------------------- 1 | package com.rpl.specter; 2 | 3 | public class MutableCell { 4 | private Object o; 5 | 6 | public MutableCell(Object o) { 7 | this.o = o; 8 | } 9 | 10 | public Object get() { 11 | return o; 12 | } 13 | 14 | public void set(Object o) { 15 | this.o = o; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/java/com/rpl/specter/Util.java: -------------------------------------------------------------------------------- 1 | package com.rpl.specter; 2 | 3 | public class Util { 4 | public static Object[] makeObjectArray(int size) { 5 | return new Object[size]; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/com/rpl/specter/cljs_test_helpers.clj: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.cljs-test-helpers) 2 | 3 | ;; it seems like gen/bind and gen/return are a monad (hence the names) 4 | (defmacro for-all+ [bindings & body] 5 | (let [parts (partition 2 bindings) 6 | vars (vec (map first parts)) 7 | genned (reduce 8 | (fn [curr [v code]] 9 | `(clojure.test.check.generators/bind ~code (fn [~v] ~curr))) 10 | `(clojure.test.check.generators/return ~vars) 11 | (reverse parts))] 12 | `(clojure.test.check.properties/for-all [~vars ~genned] 13 | ~@body))) 14 | -------------------------------------------------------------------------------- /test/com/rpl/specter/cljs_test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.cljs-test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [com.rpl.specter.core-test] 4 | [com.rpl.specter.zipper-test])) 5 | 6 | (doo-tests 'com.rpl.specter.core-test 7 | 'com.rpl.specter.zipper-test) 8 | -------------------------------------------------------------------------------- /test/com/rpl/specter/test_helpers.clj: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.test-helpers 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop]] 5 | [clojure.test]) 6 | 7 | (:use [com.rpl.specter :only [select transform]] 8 | [com.rpl.specter :only [select* transform*]])) 9 | 10 | 11 | ;; it seems like gen/bind and gen/return are a monad (hence the names) 12 | ;; this is only for clj (cljs version in different file) 13 | (defmacro for-all+ [bindings & body] 14 | (let [parts (partition 2 bindings) 15 | vars (vec (map first parts)) 16 | genned (reduce 17 | (fn [curr [v code]] 18 | `(gen/bind ~code (fn [~v] ~curr))) 19 | `(gen/return ~vars) 20 | (reverse parts))] 21 | `(prop/for-all [~vars ~genned] 22 | ~@body))) 23 | 24 | 25 | (defmacro ic-test [params-decl apath transform-fn data params] 26 | (let [platform (if (contains? &env :locals) :cljs :clj) 27 | is-sym (if (= platform :clj) 'clojure.test/is 'cljs.test/is)] 28 | `(let [icfnsel# (fn [~@params-decl] (select ~apath ~data)) 29 | icfntran# (fn [~@params-decl] (transform ~apath ~transform-fn ~data)) 30 | regfnsel# (fn [~@params-decl] (select* ~apath ~data)) 31 | regfntran# (fn [~@params-decl] (transform* ~apath ~transform-fn ~data)) 32 | params# (if (empty? ~params) [[]] ~params)] 33 | (dotimes [_# 3] 34 | (doseq [ps# params#] 35 | (~is-sym (= (apply icfnsel# ps#) (apply regfnsel# ps#))) 36 | (~is-sym (= (apply icfntran# ps#) (apply regfntran# ps#)))))))) 37 | -------------------------------------------------------------------------------- /test/com/rpl/specter/zipper_test.cljc: -------------------------------------------------------------------------------- 1 | (ns com.rpl.specter.zipper-test 2 | #?(:cljs (:require-macros 3 | [cljs.test :refer [is deftest]] 4 | [clojure.test.check.clojure-test :refer [defspec]] 5 | [com.rpl.specter.cljs-test-helpers :refer [for-all+]] 6 | [com.rpl.specter 7 | :refer [declarepath providepath select select-one select-one! 8 | select-first transform setval replace-in]])) 9 | 10 | (:use 11 | #?(:clj [clojure.test :only [deftest is]]) 12 | #?(:clj [clojure.test.check.clojure-test :only [defspec]]) 13 | #?(:clj [com.rpl.specter.test-helpers :only [for-all+]]) 14 | #?(:clj [com.rpl.specter 15 | :only [declarepath providepath select select-one select-one! 16 | select-first transform setval replace-in]])) 17 | 18 | (:require #?(:clj [clojure.test.check.generators :as gen]) 19 | #?(:clj [clojure.test.check.properties :as prop]) 20 | #?(:cljs [clojure.test.check :as tc]) 21 | #?(:cljs [clojure.test.check.generators :as gen]) 22 | #?(:cljs [clojure.test.check.properties :as prop :include-macros true]) 23 | [com.rpl.specter :as s] 24 | [com.rpl.specter.zipper :as z])) 25 | 26 | (defspec zipper-end-equivalency-test 27 | (for-all+ 28 | [v (gen/not-empty (gen/vector gen/int)) 29 | i (gen/vector gen/int)] 30 | (= (setval s/END i v) 31 | (setval [z/VECTOR-ZIP z/DOWN z/RIGHTMOST z/INNER-RIGHT] i v)))) 32 | 33 | 34 | (deftest zipper-multi-insert-test 35 | (is (= [1 2 :a :b 3 :a :b 4] 36 | (setval [z/VECTOR-ZIP 37 | z/DOWN 38 | z/RIGHT 39 | z/RIGHT 40 | (s/multi-path z/INNER-RIGHT z/INNER-LEFT)] 41 | 42 | [:a :b] 43 | [1 2 3 4]) 44 | 45 | (setval [z/VECTOR-ZIP 46 | z/DOWN 47 | z/RIGHT 48 | z/RIGHT 49 | (s/multi-path z/INNER-LEFT z/INNER-RIGHT)] 50 | 51 | [:a :b] 52 | [1 2 3 4])))) 53 | 54 | 55 | 56 | 57 | (deftest zipper-down-up-test 58 | (is (= [1 [2 3 5] 6] 59 | (transform [z/VECTOR-ZIP 60 | z/DOWN 61 | z/RIGHT 62 | z/DOWN 63 | z/RIGHT 64 | z/RIGHT 65 | (s/multi-path 66 | s/STAY 67 | [z/UP z/RIGHT]) 68 | z/NODE] 69 | inc 70 | [1 [2 3 4] 5])))) 71 | 72 | 73 | 74 | 75 | (deftest next-terminate-test 76 | (is (= [2 [3 4 [5]] 6] 77 | (transform [z/VECTOR-ZIP z/NEXT-WALK z/NODE number?] 78 | inc 79 | [1 [2 3 [4]] 5]))) 80 | (is (= [1 [3 [[]] 5]] 81 | (setval [z/VECTOR-ZIP 82 | z/NEXT-WALK 83 | (s/selected? z/NODE number? even?) 84 | z/NODE-SEQ] 85 | [] 86 | [1 2 [3 [[4]] 5] 6])))) 87 | 88 | 89 | 90 | 91 | (deftest zipper-nav-stop-test 92 | (is (= [1] 93 | (transform [z/VECTOR-ZIP z/UP z/NODE] inc [1]))) 94 | (is (= [1] 95 | (transform [z/VECTOR-ZIP z/DOWN z/LEFT z/NODE] inc [1]))) 96 | (is (= [1] 97 | (transform [z/VECTOR-ZIP z/DOWN z/RIGHT z/NODE] inc [1]))) 98 | (is (= [] 99 | (transform [z/VECTOR-ZIP z/DOWN z/NODE] inc [])))) 100 | 101 | 102 | (deftest find-first-test 103 | (is (= [1 [3 [[4]] 5] 6] 104 | (setval [z/VECTOR-ZIP 105 | (z/find-first #(and (number? %) (even? %))) 106 | z/NODE-SEQ] 107 | 108 | [] 109 | [1 2 [3 [[4]] 5] 6])))) 110 | 111 | 112 | 113 | (deftest nodeseq-expand-test 114 | (is (= [2 [2] [[4 4 4]] 4 4 4 6] 115 | (transform [z/VECTOR-ZIP 116 | z/NEXT-WALK 117 | (s/selected? z/NODE number? odd?) 118 | (s/collect-one z/NODE) 119 | z/NODE-SEQ] 120 | (fn [v _] 121 | (repeat v (inc v))) 122 | [1 [2] [[3]] 3 6])))) 123 | --------------------------------------------------------------------------------