specter/README.md

187 lines
7.8 KiB
Markdown
Raw Normal View History

2015-02-26 15:57:25 +00:00
# Specter
2015-02-26 15:55:20 +00:00
Most of Clojure programming involves creating, manipulating, and transforming immutable values. However, as soon as your values become more complicated than a simple map or list like a list of maps of maps transforming these data structures becomes extremely cumbersome.
2015-02-26 15:55:20 +00:00
2015-09-12 17:27:14 +00:00
Specter is a library (for both Clojure and ClojureScript) for doing these queries and transformations extremely concisely and elegantly. These kinds of manipulations are so common when using Clojure  and so cumbersome without Specter that Specter is in many ways Clojure's missing piece.
2015-05-11 23:44:46 +00:00
Specter is fully extensible. At its core, its just a protocol for how to navigate within a data structure. By extending this protocol, you can use Specter to navigate any data structure or object you have.
2015-09-12 17:17:06 +00:00
Specter does not sacrifice performance to achieve its elegance. Actually, Specter is faster than the limited facilities Clojure provides for doing nested operations. For example: the Specter equivalent to get-in runs 30% faster than get-in, and the Specter equivalent to update-in runs 5x faster than update-in. In each case the Specter code is equally as convenient.
2015-02-26 15:55:20 +00:00
2015-02-26 20:11:21 +00:00
# Latest Version
The latest release version of Specter is hosted on [Clojars](https://clojars.org):
[![Current Version](https://clojars.org/com.rpl/specter/latest-version.svg)](https://clojars.org/com.rpl/specter)
2015-10-11 17:33:58 +00:00
# Learn Specter
2015-02-26 15:55:20 +00:00
2015-10-11 17:33:58 +00:00
- Introductory blog post: [Functional-navigational programming in Clojure(Script) with Specter](http://nathanmarz.com/blog/functional-navigational-programming-in-clojurescript-with-sp.html)
- Talk about Specter: [Specter: Clojure's missing piece](https://www.youtube.com/watch?v=mXZxkpX5nt8)
2015-09-12 17:17:06 +00:00
2015-10-11 17:33:58 +00:00
Specter's API is contained in a single, well-documented file: [specter.cljx](https://github.com/nathanmarz/specter/blob/master/src/com/rpl/specter.cljx)
2015-02-26 19:49:42 +00:00
2015-10-11 17:33:58 +00:00
# Examples
2015-05-11 23:36:52 +00:00
2015-10-11 17:33:58 +00:00
Here's how to increment all the even values for :a keys in a sequence of maps:
2015-02-26 19:49:42 +00:00
```clojure
user> (transform [ALL :a even?]
2015-02-26 19:54:38 +00:00
inc
[{:a 1} {:a 2} {:a 4} {:a 3}])
2015-02-26 15:55:20 +00:00
[{:a 1} {:a 3} {:a 5} {:a 3}]
2015-02-26 19:49:42 +00:00
```
2015-10-11 17:33:58 +00:00
Here's how to retrieve every number divisible by 3 out of a sequence of sequences:
2015-05-11 23:36:52 +00:00
```clojure
2015-10-11 17:33:58 +00:00
user> (select [ALL ALL #(= 0 (mod % 3))]
[[1 2 3 4] [] [5 3 2 18] [2 4 6] [12]])
[3 3 18 6 12]
2015-05-11 23:36:52 +00:00
```
2015-10-11 17:33:58 +00:00
Here's how to increment the last odd number in a sequence:
2015-02-26 19:49:42 +00:00
```clojure
user> (transform [(filterer odd?) LAST]
2015-02-26 19:54:38 +00:00
inc
[2 1 3 6 9 4 8])
2015-02-26 19:49:42 +00:00
[2 1 3 6 10 4 8]
```
2015-02-26 15:55:20 +00:00
2015-10-11 17:33:58 +00:00
Here's how to increment all the odd numbers between indexes 1 (inclusive) and 4 (exclusive):
2015-04-15 03:59:04 +00:00
```clojure
user> (transform [(srange 1 4) ALL odd?] inc [0 1 2 3 4 5 6 7])
2015-04-15 03:59:04 +00:00
[0 2 2 4 4 5 6 7]
```
2015-10-11 17:33:58 +00:00
Here's how to replace the subsequence from index 2 to 4 with [-1 -1 -1]:
2015-04-15 03:59:04 +00:00
```clojure
user> (setval (srange 2 4) [-1 -1 -1] [0 1 2 3 4 5 6 7 8 9])
[0 1 -1 -1 -1 4 5 6 7 8 9]
```
Here's how to concatenate the sequence [:a :b] to every nested sequence of a sequence:
```clojure
2015-04-15 04:02:28 +00:00
user> (setval [ALL END] [:a :b] [[1] '(1 2) [:c]])
[[1 :a :b] (1 2 :a :b) [:c :a :b]]
2015-04-15 03:59:04 +00:00
```
2015-10-11 17:33:58 +00:00
Here's how to get all the numbers out of a map, no matter how they're nested:
2015-02-26 15:55:20 +00:00
```clojure
2015-02-26 19:54:38 +00:00
user> (select (walker number?)
{2 [1 2 [6 7]] :a 4 :c {:a 1 :d [2 nil]}})
2015-02-26 19:49:42 +00:00
[2 1 2 1 2 6 7 4]
2015-02-26 15:55:20 +00:00
```
2015-10-11 17:33:58 +00:00
Here's now to navigate via non-keyword keys:
2015-09-22 14:37:55 +00:00
```clojure
user> (select [(keypath "a") (keypath "b")]
{"a" {"b" 10}})
[10]
```
2015-10-11 17:33:58 +00:00
Here's how to reverse the positions of all even numbers between indexes 4 and 11:
```clojure
user> (transform [(srange 4 11) (filterer even?)]
reverse
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15])
[0 1 2 3 10 5 8 7 6 9 4 11 12 13 14 15]
```
Here's how to decrement every value in a map:
```clojure
user> (transform [ALL LAST]
dec
{:a 1 :b 3})
{:b 2 :a 0}
```
Here's how to append [:c :d] to every subsequence that has at least two even numbers:
```clojure
user> (setval [ALL
(selected? (filterer even?) (view count) #(>= % 2))
END]
[:c :d]
[[1 2 3 4 5 6] [7 0 -1] [8 8] []])
[[1 2 3 4 5 6 :c :d] [7 0 -1] [8 8 :c :d] []]
```
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:
2015-02-26 19:49:42 +00:00
2015-02-26 15:55:20 +00:00
```clojure
user> (transform [ALL (collect-one :b) :a even?]
2015-02-26 19:54:38 +00:00
+
[{:a 1 :b 3} {:a 2 :b -10} {:a 4 :b 10} {:a 3}])
2015-02-26 15:55:20 +00:00
[{:b 3, :a 1} {:b -10, :a -8} {:b 10, :a 14} {:a 3}]
```
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.
2015-02-26 19:49:42 +00:00
2015-05-27 05:05:15 +00:00
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.
2015-02-26 19:49:42 +00:00
2015-10-11 17:33:58 +00:00
Here's how to increment the value for :a key by 10:
2015-02-26 19:49:42 +00:00
```clojure
2015-10-11 17:33:58 +00:00
user> (transform [:a (putval 10)]
+
{:a 1 :b 3})
{:b 3 :a 11}
2015-02-26 19:49:42 +00:00
```
2015-10-11 17:33:58 +00:00
For every map in a sequence, increment every number in :c's value if :a is even or increment :d if :a is odd:
```clojure
2015-10-11 17:33:58 +00:00
user> (transform [ALL (if-path [:a even?] [:c ALL] :d)]
inc
[{:a 2 :c [1 2] :d 4} {:a 4 :c [0 10 -1]} {:a -1 :c [1 1 1] :d 1}])
[{:c [2 3], :d 4, :a 2} {:c [1 11 0], :a 4} {:c [1 1 1], :d 2, :a -1}]
```
2015-10-11 17:33:58 +00:00
You can make `select` and `transform` work much faster by precompiling your selectors using the `comp-paths` function. There's about a 3x speed difference between the following two invocations of transform:
2015-02-26 19:49:42 +00:00
```clojure
(def precompiled (comp-paths ALL :a even?))
2015-02-26 19:49:42 +00:00
(transform [ALL :a even?] inc structure)
(compiled-transform precompiled inc structure)
2015-02-26 19:49:42 +00:00
```
Depending on the details of the selector and the data being transformed, precompiling can sometimes provide more than a 10x speedup.
You can even precompile selectors that require parameters! For example, `keypath` can be used to navigate into a map by any arbitrary key, such as numbers, strings, or your own types. One way to use `keypath` would be to parameterize it at the time you use it, like so:
```clojure
(defn foo [data k]
(select [(keypath k) ALL odd?] data))
```
2015-09-12 17:17:06 +00:00
It seems difficult to precompile the entire path because it is dependent on the argument `k` of `foo`. Specter gets around this by allowing you to precompile a path without its parameters and bind the parameters to the selector later, like so:
```clojure
(def foo-path (comp-paths keypath ALL odd?))
(defn foo [data k]
(compiled-select (foo-path k) data))
```
This code will execute extremely efficiently.
When `comp-paths` is used on selectors that require parameters, the result of `comp-paths` will require parameters equal to the sum of the number of parameters required by each selector. It expects to receive those parameters in the order in which the selectors were declared. This feature, called "late-bound parameterization", also works on selectors which themselves take in selector paths, such as `selected?`, `filterer`, and `transformed`.
2015-06-19 18:21:52 +00:00
2015-02-26 15:55:20 +00:00
# Future work
2015-07-01 14:40:44 +00:00
- Integrate Specter with other kinds of data structures, such as graphs
- Make it possible to parallelize selects/transforms
2015-02-26 19:49:42 +00:00
- Any connection to transducers?
2015-02-26 20:11:21 +00:00
# License
2015-02-26 19:49:42 +00:00
2015-02-27 16:21:58 +00:00
Copyright 2015 Red Planet Labs, Inc. Specter is licensed under Apache License v2.0.