Specter Solves Clojure Nested Data Bottleneck via Path Navigation

Understanding Specter: Clojure's missing piece — Misophistful

Misophistful

Specter eliminates multi-line boilerplate and preserves collection types by playing back navigation paths in reverse to update nested values in place.

AI-assisted summary of Understanding Specter: Clojure's missing piece — original source. Check the original for full context.

Source published:

Key ideas

  • Specter addresses Clojure's tendency to strip collection types during transformations by automatically preserving the original data structure (e.g., sets remain sets, vectors remain vectors) without requiring custom conversion logic. (1:21)
  • Unlike standard Clojure core functions, which struggle with nested data and require verbose boilerplate, Specter uses composable path navigators to query, filter, and transform deep structures in one to three lines of code. (3:28)
  • Specter's core mechanism operates via a two-phase pipeline: it records a navigation path down to targeted elements, applies the transformation, and replays the path backwards to reconstruct the surrounding data structure. (11:41)
  • The library supports advanced navigation capabilities including positional insertion (BEFORE-INDEX, AFTER-ELEM), recursive tree extraction (WALKER), and bi-directional transformations on serialized types like strings via parser pairs. (27:37)

Detailed notes

## Background "Understanding Specter: Clojure's missing piece" was published on YouTube by Misophistful on October 6, 2017. The tutorial screencast aims to teach Clojure developers how to elegantly manipulate nested data structures using Nathan Marz's Specter library.

## TL;DR The video examines two major friction points in standard Clojure—collection type preservation and clumsy nested data manipulation—and demonstrates how Specter resolves them through its path navigation abstraction. The presenter explains Specter's two-phase navigation and playback reconstruction pipeline, contrasting it with core primitives like get-in. Through interactive REPL examples, the screencast explores queries and updates using transform, select, and setval, walks through positional insertions with navigators like BEFORE-INDEX, and demonstrates arbitrary tree searches with WALKER. Finally, the session covers bi-directional string parsing and transformations using paired parse and unparse functions alongside the parser navigator.

## Atmosphere & style The presentation carries the warm, practical tone of an enthusiastic engineer sharing a favorite tool during a live REPL coding session. The host maintains an approachable, unpretentious demeanor, candidly admitting when deep nesting makes navigation tedious and openly admiring the elegance of Specter's underlying design. Watching the tutorial feels exploratory and satisfying, as tangled boilerplate Clojure code is repeatedly distilled into concise, expressive navigation paths.

## Bulletin - [01:21](https://www.youtube.com/watch?v=rh5J4vacG98&t=81s) Specter addresses Clojure's tendency to strip collection types during transformations by automatically preserving the original data structure (e.g., sets remain sets, vectors remain vectors) without requiring custom conversion logic. - [03:28](https://www.youtube.com/watch?v=rh5J4vacG98&t=208s) Unlike standard Clojure core functions, which struggle with nested data and require verbose boilerplate, Specter uses composable path navigators to query, filter, and transform deep structures in one to three lines of code. - [11:41](https://www.youtube.com/watch?v=rh5J4vacG98&t=701s) Specter's core mechanism operates via a two-phase pipeline: it records a navigation path down to targeted elements, applies the transformation, and replays the path backwards to reconstruct the surrounding data structure. - [27:37](https://www.youtube.com/watch?v=rh5J4vacG98&t=1657s) The library supports advanced navigation capabilities including positional insertion (BEFORE-INDEX, AFTER-ELEM), recursive tree extraction (WALKER), and bi-directional transformations on serialized types like strings via parser pairs.

## Key takeaways - [01:21](https://www.youtube.com/watch?v=rh5J4vacG98&t=81s) Standard Clojure core functions like `map` discard concrete collection types in favor of lazy sequences, requiring manual workarounds like `mapv` that still fail to generically preserve types like sets. - [03:28](https://www.youtube.com/watch?v=rh5J4vacG98&t=208s) Manipulating nested data structures in idiomatic Clojure requires awkward boilerplate involving manual destructuring, filtering, mapping, and repacking with `into`. - [07:58](https://www.youtube.com/watch?v=rh5J4vacG98&t=478s) Specter generalizes the path lookup concept found in Clojure's core `get-in`, elevating simple key lookups into a full-featured navigation and querying language. - [11:41](https://www.youtube.com/watch?v=rh5J4vacG98&t=701s) The `transform` function navigates down to specific nested values, applies an update function, and reconstructs the enclosing collections via reverse playback while preserving untouched values. - [15:36](https://www.youtube.com/watch?v=rh5J4vacG98&t=936s) The `select` macro enables read-only queries against deeply nested structures, allowing developers to inspect data at intermediate navigational paths without mutating it. - [15:36](https://www.youtube.com/watch?v=rh5J4vacG98&t=936s) When building Specter navigation paths, developers must carefully track whether a navigator points to an individual collection element or the collection sequence itself. - [23:23](https://www.youtube.com/watch?v=rh5J4vacG98&t=1403s) The `setval` macro provides a clean convenience alternative to `transform`, replacing targeted elements with static values without wrapping them in `constantly`. - [27:37](https://www.youtube.com/watch?v=rh5J4vacG98&t=1657s) Specter allows positional navigation into empty slots between or around sequence items using navigators like `BEFORE-ELEM`, `AFTER-ELEM`, and `BEFORE-INDEX`. - [32:44](https://www.youtube.com/watch?v=rh5J4vacG98&t=1964s) The `WALKER` navigator recursively traverses arbitrary tree structures, pulling out leaves matching specified predicates without requiring explicit structural paths. - [34:24](https://www.youtube.com/watch?v=rh5J4vacG98&t=2064s) Intra-value transformations, such as modifying substring components within dates or times, are handled losslessly through bi-directional `parser` navigators requiring matching `parse` and `unparse` functions.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=rh5J4vacG98&t=0s) Introduction to Specter and Screencast Overview

The screencast introduces Specter, a data manipulation library for Clojure authored by Nathan Marz. The presenter expresses deep admiration for the project, noting that it is not only a personal favorite used constantly, but also stands out in broader software engineering terms. As the speaker puts it: “I want to take a little bit of your time to explain why it's one of my favorite Clojure libraries, why I use it all the time, and even why I think it's one of the best pieces of software engineering ever produced.”

The session is organized into three distinct parts: 1. Explaining the motivation behind Specter and why Clojure developers should take an interest in it. 2. Unpacking how Specter works conceptually under the hood. 3. Demonstrating concrete code examples to illustrate its utility and expressive power.

The presenter sets expectations regarding the audience: the talk is aimed primarily at developers who already have a working knowledge of Clojure but have not yet explored Specter deeply. Specter experts might find much of the foundational material already familiar, while complete Clojure beginners might prefer a gentler introductory resource before tackling the advanced patterns shown here.

[01:21](https://www.youtube.com/watch?v=rh5J4vacG98&t=81s) The First Weakness of Clojure: Type Preservation in Collections

While the presenter views Clojure as an exceptionally well-designed language with very few flaws, there are two specific areas where daily development becomes noticeably painful. The first major friction point is Clojure's struggle to maintain original collection types when using standard higher-order functions like `map` or `filter`. When a developer passes a vector of numbers into `map`, the expression evaluates to a lazy sequence (rendered as a list) rather than returning the vector originally provided. Although there are deliberate historical design rationales from Rich Hickey for returning lazy sequences, in typical application logic developers frequently expect an input vector to produce an output vector without stripping away structural identity.

Standard Clojure offers partial workarounds, but they lack generic applicability. Using `mapv` guarantees a vector output, yet it forces the function to assume a vector type explicitly. If a caller passes a set into `mapv`, the output is still coerced into a vector rather than preserving the set. Handling this generically across multiple data structures inevitably forces the programmer to write conditionals or custom conversion logic. Specter solves this out of the box using its core `transform` function. By passing a navigator (such as `ALL`), a transformation function like `inc`, and an input collection (such as a set `#{1 2 3 4}` or a vector), Specter automatically retains and returns the exact data structure type supplied without extra configuration.

```mermaid flowchart TD accTitle: Collection Type Handling Comparison accDescr: Comparing standard Clojure mapping behavior with Specter transform behavior across data types.

subgraph Core["Standard Clojure Core"] V1["Vector: [1 2 3]"] -->|map inc| S1["LazySeq: (2 3 4)"] S2["Set: #{1 2 3}"] -->|mapv inc| V2["Vector: [2 3 4]"] end

subgraph Spec["Specter transform"] V3["Vector: [1 2 3]"] -->|ALL, inc| V4["Vector: [2 3 4]"] S3["Set: #{1 2 3}"] -->|ALL, inc| S4["Set: #{2 3 4}"] end ```

[03:28](https://www.youtube.com/watch?v=rh5J4vacG98&t=208s) The Second Weakness: Manipulating Nested Data Structures

Type retention is merely a helpful auxiliary benefit; Specter's primary purpose is resolving Clojure's clumsy handling of nested data structures. Because Clojure's syntax makes constructing deeply nested maps and vectors effortless, nested data appears everywhere in real-world codebases. However, the standard library is only well-suited for unnested collections. The speaker observes: “Unfortunately the default syntax and core library in Clojure is not really elegant at handling nested data structures; it's really good at dealing with unnested data structures, but as soon as you even put in some kind of basic nesting, suddenly it becomes a nightmare to maintain the structure and to traverse over it.”

To illustrate this shortcoming, the presenter constructs a minimal nested data structure consisting of a vector containing three hash maps:

```clojure [{:a 1:b 2} {:c 3:d 4} {:e 5:f 6}] ```

The objective is straightforward: traverse the data structure and increment only the even numbers while leaving odd numbers and the surrounding structure completely untouched. Accomplishing this with pure Clojure requires building boilerplate helper functions: - An `apply-function-to-hash-map` function that takes a function and a map, runs a `for` comprehension destructuring `[k v]` pairs, maps `(f v)` onto the values, and repacks the sequence into a hash map via `(into {}...)`. - A conditional helper function like `inc-even` taking `n` and evaluating `(if (even? n) (inc n) n)`. - A wrapping call that maps `apply-function-to-hash-map` over the vector, switching from `map` to `mapv` to preserve the outer vector rather than returning a lazy sequence.

This standard implementation quickly balloons into multi-line, fragile, and hard-to-read code just to modify values one level deep inside maps.

[07:58](https://www.youtube.com/watch?v=rh5J4vacG98&t=478s) The Specter Navigation Abstraction and `get-in` Comparison

By contrast, the presenter demonstrates the equivalent operation written in Specter:

```clojure (transform [ALL MAP-VALS even?] inc data) ```

In a single concise expression, Specter navigates through each map in the vector (`ALL`), inspects every value within those maps (`MAP-VALS`), filters exclusively for the even numbers (`even?`), and applies `inc` to them. It yields the exact modified nested vector without requiring any custom destructuring, repacking, or intermediate helper functions.

The presenter acknowledges that the path navigation paradigm can initially feel foreign to Clojure developers accustomed to standard sequencing and threading macros: “I know for me it took me a while to grok it, to understand it, but once you do get it, you realize how simple and powerful a way of describing navigating around the data it really is, and you really fall in love with it.”

To make this concept intuitive, the presenter anchors it in a core Clojure function that developers already know well: `get-in`. When extracting a nested value from nested maps—such as finding the value under key `:c` in `{:a {:b {:c 1}}}`—a developer passes a path vector `[:a:b:c]` to `get-in`. Specter generalizes this exact concept of a path, elevating it from simple key lookups into a full-featured navigation language capable of arbitrary traversals, conditional queries, and updates across nested structures.

[10:40](https://www.youtube.com/watch?v=rh5J4vacG98&t=640s) Basic Navigators vs. Clojure's Core `get-in`

The speaker begins by verifying that standard Clojure access functions behave as expected on nested maps. A lookup returning `1` serves as a baseline:

```clojure ;; Conceptual equivalent of navigating nested structures in core Clojure (get-in [{:b {:c 1}}:foo] [0:b:c]) ;; => 1 ```

The speaker points out that while `get-in` feels like a primitive ancestor of Specter's navigator concept, it remains strictly limited in scope:

> “This here is you can think that is a very basic version of Specter's navigator concept this navigates you through the nested maps but with `get-in` it only works with the keyword and I think it also works with a sequence...”

Testing this in the REPL, the speaker confirms that `get-in` accepts sequential indexing alongside keyword lookups, correctly pulling out `1` when passing a path like `[0:b:c]`. However, this native mechanism cannot flexibly express complex navigation patterns across arbitrary combinations of collections, filtering predicates, and structural transformations in the way Specter does.

[11:41](https://www.youtube.com/watch?v=rh5J4vacG98&t=701s) How `transform` Navigates and Reconstructs Data

To demonstrate Specter's internals, the speaker pastes a previously prepared example using `transform` on a vector of maps:

```clojure ;; Initial structure: [{:a 1:b 2} {:c 3} {:d 4}]

;; Specter transformation: (transform [ALL MAP-VALS even?] inc data) ```

The speaker steps through each phase of navigation to explain how the data structure is traversed. Specter strips away the enclosing vector using `ALL`, targeting all three maps simultaneously while keeping them distinct. Next, `MAP-VALS` navigates to all the map values across those maps—producing the individual scalar values `1`, `2`, `3`, and `4`. Applying the predicate `even?` filters that target set down strictly to `2` and `4`. At the end of this navigation path, Specter runs the transformation function `inc`, incrementing `2` to `3` and `4` to `5`.

```mermaid flowchart TD accTitle: Specter Transform and Reconstruction Flow accDescr: Shows navigation down to target values, transformation, and reconstruction back up.

subgraph Navigation ["1. Path Navigation Down"] A["'[{:a 1:b 2} {:c 3} {:d 4}]'"] -->|"ALL"| B["Maps: '{:a 1:b 2}', '{:c 3}', '{:d 4}'"] B -->|"MAP-VALS"| C["Values: 1, 2, 3, 4"] C -->|"even?"| D["Filtered Targets: 2, 4"] end

subgraph Transform ["2. Transformation"] D -->|"inc"| E["Updated Values: 3, 5"] end

subgraph Reconstruction ["3. Playback Reconstruction Up"] E -->|"Re-merge with odds"| F["Values: 1, 3, 3, 5"] F -->|"Rebuild Maps"| G["Maps: '{:a 1:b 3}', '{:c 3}', '{:d 5}'"] G -->|"Rebuild Vector"| H["'[{:a 1:b 3} {:c 3} {:d 5}]'"] end ```

The speaker highlights the subsequent "reconstruction phase" as the true ingenuity of the library:

> “And then he goes into its reconstruction phase and this is where the real magic happens and this is where I'm not 100% sure on how it works because I've not actually looked into the code it's more from my understanding from Nathan's videos and reading around but what I think happens is this: what it does is it just plays what it recorded in terms of the path backwards to reconstruct back to where you were but along the way it replaces the values that you've updated with your transformation function.”

By playing back the navigation trail in reverse, Specter re-associates the updated values (`3` and `5`) alongside the untouched filtered-out values (`1` and `3`), reconstructs the maps, and finally reassembles the outer vector. Beyond its conceptual cleanliness, the speaker praises Nathan Marz's engineering, noting that hand-rolled Clojure code routinely struggles to match Specter's execution speed.

[15:36](https://www.youtube.com/watch?v=rh5J4vacG98&t=936s) Querying Complex Nested Data with `select`

Specter's path navigators are not restricted to data transformation; by stopping at the end of a navigation path without providing an update function, they act as query mechanisms using `select`. To demonstrate this, the speaker imports a realistic, deeply nested JSON payload that has been converted into Clojure data: a root map containing a `:res` key mapped to a vector of maps, which contain nested `:catalysts` vectors of maps, which in turn contain `:points` vectors containing `:start`, `:stop`, and `:point` entries.

The speaker points out that trying to query or reshape this structure using standard Clojure functions would result in deeply nested, convoluted `map` and `reduce` pipelines. Using Specter, querying for specific properties is straightforward:

```clojure ;; Initial attempt querying all points (select [:res ALL:catalysts ALL:points] data) ```

To restrict the result to only the `:start` and `:stop` keys from each map, the speaker first attempts to append `(submap [:start:stop])` directly after `:points`:

```clojure ;; Fails because:points yields a vector/sequence of maps, not a single map element (select [:res ALL:catalysts ALL:points (submap [:start:stop])] data) ```

The query fails because `:points` points to a vector of elements rather than an individual map element. The speaker explains the critical rule when constructing Specter paths:

> “...and this is what I was talking about before about you need to keep track of are you navigating to an element or to a sequence of elements and because points is actually a sequence of maps that was fine before when we navigated there cuz it just returned the sequence as part of the result but now we want to do a `submap` on all of those maps...”

By inserting `ALL` after `:points` to navigate down into each individual map within the vector—`[:res ALL:catalysts ALL:points ALL (submap [:start:stop])]`—the query evaluates successfully, isolating just the start and stop times across the entire nested structure.

[19:37](https://www.youtube.com/watch?v=rh5J4vacG98&t=1177s) Transforming Deeply Nested Structures: Reversing by `:stop` Time

Moving from read-only queries back to transformation, the speaker shows how the same deep path navigation can update data deep within the hierarchy without disturbing unrelated keys. The goal is to sort each list of `:points` in reverse order based on their `:stop` timestamp.

The speaker constructs the transformation path to target the `:points` vectors:

```clojure (transform [:res ALL:catalysts ALL:points] (comp reverse (partial sort-by:stop)) data) ```

The speaker initially runs `(sort-by:stop)` alone, inspects the REPL output, and notes that because the sample data happened to already be sorted chronologically (e.g., `13:35` followed by `14:35`), no visible change took place. To make the effect obvious, the speaker changes the transformation function to reverse the sort order. Drilling down into the resulting data structure under `:catalysts` and `:points`, the speaker verifies the change:

> “...now hopefully there will be reversed if we drill all the way down again the catalyst cat points yes they've swapped over see see now point two comes first and point one comes second okay so let's go back and figure out what we've actually done here...”

Because the path ended at `:points` (without an extra `ALL`), the transformation function received the entire sequence of point maps at once, enabling it to reorder the sequence in place before Specter reconstructed the surrounding hierarchy intact.

[22:09](https://www.youtube.com/watch?v=rh5J4vacG98&t=1329s) Reversing Vectors and the Expressiveness of Specter

Tracing the flow of the preceding transformation, Specter reaches into the nested structure step-by-step. It navigates down to the target data structure—a vector of maps representing points—and passes that entire vector directly into the transformation function. Inside that function, the data is sorted by the `:stop` time key and then reversed. The updated collection slots cleanly back into the original data layout with all surrounding contexts intact.

```mermaid flowchart LR accTitle: Vector Transformation Flow accDescr: Illustrates Specter navigating to a vector of points, applying sort and reverse, and returning the updated structure. subgraph Navigation ["Specter Navigation"] A["Root Data"] --> B["Nested Path"] B --> C["Target: Vector of Point Maps"] end subgraph Transformation ["Transform Function"] C --> D["sort-by:stop"] D --> E["reverse"] end subgraph Result ["Reconstruction"] E --> F["Replaced in-place"] end ```

The speaker emphasizes how concise and maintainable this paradigm becomes once the syntax clicks. Because you only need to declare where you are navigating and what function you want to apply, complex structural mutations take just one to three lines of code. As the speaker puts it: “As you can see you can do something incredibly powerful with very little code that once you get used to reading it is really highly readable because the these navigations once you get used to the syntax and how it works on what you're looking for it doesn't take long at all to realize how you've navigated through the structure.”

- The transformation function receives the target vector, runs `sort-by` on `:stop`, applies `reverse`, and returns the modified vector to the original position. - Specter leaves all non-targeted surrounding data untouched. - Operations that typically take dozens of lines of nested Clojure manipulation reduce to one to three lines.

---

[23:23](https://www.youtube.com/watch?v=rh5J4vacG98&t=1403s) Replacing Values Directly with `setval`

While `transform` shines when an existing value needs to be passed to a function to compute a replacement, many data manipulation tasks simply involve overwriting a target with a static value. If one were to use `transform` for this, it would require supplying an anonymous function or using `constantly` to ignore the incoming parameter (e.g., `(transform [:timestamp] (constantly 12345) data)`). To make this cleaner, Specter author Nathan Marz provided the helper macro `setval`.

To demonstrate, the speaker targets the `:timestamp` key at the top level of the data structure. Using `(setval:timestamp 123456 data)`, the timestamp updates immediately while leaving the rest of the data structure untouched. The speaker contrasts this with `transform`, illustrating that `setval` acts as a direct convenience function that removes the need for `constantly`: “What `setval` does it's it's just the convenience function where you don't need to put constantly you can just put the value that you want to update it to and that works well for a single value like that but it also works really well you have a collection of values.”

---

[25:19](https://www.youtube.com/watch?v=rh5J4vacG98&t=1519s) Navigating Paths with `FIRST` and Overwriting Map Elements

The speaker shifts from setting top-level keys to targeting specific elements deep within nested sequences using built-in navigators like `FIRST`. Rather than operating across every element with `ALL`, the task is to navigate specifically to the first group within `:res`, down to the first item inside `:catalysts`, and then into `:points` to overwrite its first point map.

```mermaid flowchart TD accTitle: Targeted Element Overwrite accDescr: Step-by-step navigation down to the first point of the first catalyst of the first group to replace it. R[":res"] --> G1["FIRST (Group 1)"] G1 --> C[":catalysts"] C --> Cat1["FIRST (Catalyst 1)"] Cat1 --> P[":points"] P --> P1["FIRST (Point 1)"] P1 -->|"setval {:point:x}"| NewP["Replaced with Point X"] ```

The resulting Specter path uses `[:res FIRST:catalysts FIRST:points FIRST]`. When paired with `setval` and a new map—such as `{:point "X"}`—Specter traverses straight to the first point map and substitutes the new map in place of the original `point-1` start and stop entries. Because the navigator stopped at an individual element, replacing it with a new map cleanly replaces that single element rather than splicing or reshaping the parent vector.

- Navigators can drill down selectively: `[:res FIRST:catalysts FIRST:points FIRST]`. - Supplying a new map `{:point "X"}` via `setval` replaces only that single element. - The sibling points and all surrounding metadata in the catalyst remain preserved.

---

[27:37](https://www.youtube.com/watch?v=rh5J4vacG98&t=1657s) Inserting into Positions: `BEFORE-ELEM`, `AFTER-ELEM`, and `BEFORE-INDEX`

Beyond navigating directly to elements or sequences, Specter can navigate to specific *positions* or empty spaces between elements, allowing you to insert data before, between, or after existing values without overwriting them. The speaker explains: “Specter has an ability not just to navigate to a specific element or a specific sequence within a date structure but can actually navigate to a specific position and once you're at that position then if you do an insert then you can place things in between before or after which is really powerful.”

```mermaid flowchart LR accTitle: Positional Navigation for Insertions accDescr: Showing how BEFORE-ELEM, BEFORE-INDEX, and AFTER-ELEM position pointers for inserting items into a sequence. subgraph PointsVector ["Vector of Points"] P0["[0] Point 1"] P1["[1] Point 2"] end BE["BEFORE-ELEM"] -->|"Inserts at index 0"| P0 BI["BEFORE-INDEX 1"] -->|"Inserts between 0 and 1"| P0 BI -->|"Inserts between 0 and 1"| P1 AE["AFTER-ELEM"] -->|"Inserts at end"| P1 ```

The speaker experiments with various positional navigators: 1. **`AFTER-ELEM`**: Targeting `[:res FIRST:catalysts FIRST:points AFTER-ELEM]` and calling `setval` with `{:point "Z"}` appends the new point after the existing elements, expanding the collection from two points (`point-1`, `point-2`) to three (`point-1`, `point-2`, `point-Z`). 2. **`BEFORE-ELEM`**: Switching the navigator to `BEFORE-ELEM` inserts `point-Z` at the head of the vector. The speaker humorously apologizes for having nested the sample data so deeply: “Oh I did I put it so far nested down it takes us ages to find it each time sorry about that yeah that is expected now point Z is at the beginning of the sequence of this vector of maps not at the end.” 3. **`INDEXED-PATH` vs `BEFORE-INDEX`**: Wanting to insert an element in the middle, the speaker initially checks `nthpath` (or `INDEXED-PATH`), but realizes that navigates directly *to* the specified element index (replacing it like `nth`), rather than targeting the empty slot between indices. Checking Nathan Marz's documentation on GitHub and the Specter wikis for navigators and macros, the speaker finds `(BEFORE-INDEX 1)`. Running `setval` targeting `(BEFORE-INDEX 1)` successfully injects the new point cleanly between index `0` and index `1`.

---

[32:44](https://www.youtube.com/watch?v=rh5J4vacG98&t=1964s) Querying Arbitrary Tree Structures with `WALKER`

To conclude the demonstration, the speaker highlights Specter's built-in `WALKER` navigator, which recursively traverses an entire data structure without needing an explicit path schema. This is especially useful when data structures behave like arbitrary trees and you need to query or collect specific data types from leaf nodes.

Using `select` combined with `(WALKER number?) data`, Specter walks the entire tree and extracts every single number into a flat vector. Changing the predicate from `number?` to `string?` immediately pulls out all string values across every level of the structure. The speaker summarizes the utility: “Walker is a fantastic way to navigate to all of the nested leaves of any deeply nested data structure which tends to look like a tree and just make sure that you get all of them out and then you can apply a predicate to make sure you only see the ones that you care about.” The discussion then pivots toward parsing and transforming strings encountered along paths.

- `(select [(WALKER number?)] data)` extracts all numbers across arbitrary nesting depths. - `(select [(WALKER string?)] data)` pulls every string leaf across the structure. - The Specter GitHub repository provides comprehensive examples, video presentations by Nathan Marz, and detailed wiki pages covering navigators and macros.

[34:24](https://www.youtube.com/watch?v=rh5J4vacG98&t=2064s) Bi-Directional Transformations with `parser`, `parse`, and `unparse`

The host shifts focus to string manipulation inside nested data structures, taking on a specific challenge: rounding down all stop times to the nearest hour. If an entry has a stop time string like `"10.35"`, the goal is to transform it into `"10.00"`. Specter handles this kind of intra-value navigation through a bi-directional parsing mechanism that requires two companion functions: a `parse` function and an `unparse` function. Because Specter works by drilling down the navigational path to isolate target elements and then reconstructing the data structure on the way back up, it relies on this paired structure to keep transformations lossless and seamless.

As the host explains the underlying mechanics: “it as you navigate down and you do a pass it will call your pass function to split things up however you want and then you can continue manipulating it but then when it's reconstructing and doing the reverse it needs an unpassed function to put them back together”. Implementing this in Clojure requires splitting and joining strings:

```mermaid flowchart LR accTitle: Bi-directional Specter Parsing Flow accDescr: Navigation splits the target string via parse, modifies the value, and reconstructs the string via unparse. subgraph Downward ["Navigation Downwards"] A["Raw String<br/>'10.35'"] -->|"parse (str/split)"| B["Vector<br/>['10', '35']"] B -->|"LAST"| C["Minute String<br/>'35'"] end subgraph Upward ["Reconstruction Upwards"] C -->|"set-val '00'"| D["Modified Vector<br/>['10', '00']"] D -->|"unparse (str/join)"| E["Final String<br/>'10.00'"] end ```

- **The `parse` function**: Defined using `clojure.string/split`, taking the time string and splitting it on the literal dot character (`\\.`, escaping the regex dot wildcard). When passed `"10.35"`, it successfully evaluates to a two-element vector: `["10" "35"]`. - **The `unparse` function**: Defined using `clojure.string/join`, taking a dot `"."` separator and the collection `split-time` vector of strings. Calling `unparse` on `["10" "35"]` evaluates back to the joined string `"10.35"`, providing the exact inverse operation required by Specter.

[37:03](https://www.youtube.com/watch?v=rh5J4vacG98&t=2223s) Navigating and Rounding Down Values with `parser` and `LAST`

To apply this logic to the nested trip data structure, the host demonstrates how to assemble the Specter path using `set-val` (interchangeably tested with `select`). Navigating down past the points into each map, the path targets `:stop-time`. At that exact point, Specter provides the built-in navigator `parser`, which accepts the custom `parse` and `unparse` functions as arguments. By invoking `parser` inline, Specter translates each stop time string into its split vector representation on the fly during navigation.

The host emphasizes how useful `select` is during development: “you can always use select as you're building up your transforms or your cells to see where you've got to”. Inspecting the navigation with `select` confirms that the path lands squarely on vectors of split string components. From there, Specter's standard navigators can continue drilling deeper into the parsed data. Appending the navigator `LAST` targets specifically the minute position (`"35"`). Switching back from `select` to `set-val`, the host replaces all targeted minute values with `"00"`. Re-evaluating the full structure confirms that previous values like `"13.35"` and `"2.35"` are cleanly updated to `"13.00"` and `"2.00"`.

- `parser` bridges the boundary between nested data structures and string-level manipulation, allowing further Specter navigators (such as `LAST`) to operate directly inside the parsed vector. - The host concedes that rounding time strings like this is “a bit of a contrived example you might not do it in this way but it's just to show you how you can use other functions with your navigator to change how you're seeing the data and then you can manipulate it”. - The host highlights the power of Specter's automatic structural restoration: “and because of how Specter magically puts it all back together for you you get very powerful results with very little code”.

[40:02](https://www.youtube.com/watch?v=rh5J4vacG98&t=2402s) Conclusion and Enthusiasm for Specter

The video concludes with an endorsement of the library's utility across Clojure development. The host notes, “I'm very excited about Specter I use it all the time in all the projects that I write in Clojure”, having demonstrated why Specter is so compelling, how its two-phase navigation and reconstruction pipeline works, and how navigators like `parser`, `select`, and `set-val` simplify complex manipulations. Expressing the hope that viewers have caught the excitement and will explore the library in their own codebases, the host thanks the audience and signs off.

## Mentioned Specter, Clojure, Nathan Marz, Rich Hickey, JSON, Atom, proto-repl, GitHub

## Video description Learn how to elegantly manipulate nested data structures with Specter.

Specter: https://github.com/nathanmarz/specter

Language: http://clojure.org Editor: https://atom.io/ proto-repl: https://atom.io/packages/proto-repl proto-repl setup guide: https://gist.github.com/jasongilman/d... Atom syntax theme: https://atom.io/themes/borealis-syntax

[Source](https://www.youtube.com/watch?v=rh5J4vacG98)

中文简报

Specter通过路径导航解决Clojure嵌套数据瓶颈 Specter通过逆向重放导航路径就地更新嵌套值,消除了多行样板代码并保留了集合类型。 该视频探讨了标准Clojure中的两大主要痛点——集合类型保留以及繁琐的嵌套数据操作,并展示了Specter如何通过其路径导航抽象来解决这些问题。演讲者解释了Specter的两阶段导航与重放重构管线,并将其与get-in等核心原语进行了对比。通过交互式REPL示例,屏幕录像演示了使用transform、select和setval进行的查询与更新,介绍了使用BEFORE-INDEX等导航器进行位置插入,并展示了使用WALKER进行任意树搜索。最后,该环节还介绍了使用成对的parse与unparse函数配合parser导航器实现双向字符串解析和转换。 针对Clojure在转换过程中容易丢失集合类型的问题,Specter无需自定义转换逻辑即可自动保留原始数据结构(例如,集合仍为集合,向量仍为向量)。 (1:21) 与处理嵌套数据较为吃力且需要冗长样板代码的Clojure标准核心函数不同,Specter使用可组合的路径导航器,仅需一到三行代码即可查询、过滤和转换深层结构。 (3:28) Specter的核心机制通过两阶段管线运行:它记录向下深入目标元素的导航路径,应用转换,随后逆向重放该路径以重构外层数据结构。 (11:41) 该库支持高级导航功能,包括位置插入(BEFORE-INDEX、AFTER-ELEM)、递归树提取(WALKER),以及通过parser对在字符串等序列化类型上执行双向转换。 (27:37)

Reading summary

The presenter argues that while Clojure is an exceptionally well-designed language, it struggles with collection type preservation and nested data manipulation. Nathan Marz's Specter library resolves these frictions by generalizing path navigation into a declarative framework that traverses, transforms, and reconstructs nested data in place. Clojure's Nested Data and Type Pitfalls (1:21) Clojure's standard higher-order functions often fail to maintain collection types during transformations. Passing a vector into core `map` yields a lazy sequence rather than the original vector, while `mapv` forces the output into a vector even when supplied with a set. Handling type preservation generically across varying data structures typically forces developers into writing bespoke conditionals and conversion functions. Specter circumvents this by retaining the input collection type automatically across operations like `transform`. A more pronounced friction occurs when working with nested data structures. While Clojure makes constructing deeply nested maps and vectors effortless, modifying them using core abstractions requires substantial boilerplate. Incrementing only even numbers inside a vector of maps, for instance, requires hand-rolled helper functions combining `for` comprehensions, key-value destructuring, `into` repacking, and conditional checks. This standard approach quickly balloons into multi-line, brittle code. Path Navigation and Reversible Reconstruction (7:58) Specter resolves nested data manipulation by generalizing Clojure's native `get-in` path concept into a comprehensive navigation language. Rather than relying on multi-step mapping pipelines, an operation like incrementing even numbers across a vector of maps is expressed as `(transform [ALL MAP-VALS even?] inc data)`. Specter navigates across every map in the vector (`ALL`), targets the values of each map (`MAP-VALS`), filters down to the even values (`even?`), and executes `inc`. The library achieves this through a two-phase process: it navigates down to isolate target elements, applies the transformation, and executes a reverse reconstruction phase. By playing back the recorded navigation path backwards, Specter substitutes the updated values while merging untouched data and reassembling the parent structures intact. The speaker highlights Nathan Marz's underlying engineering, noting that hand-written Clojure code rarely matches Specter's execution speed. Querying, Precise Mutation, and Positional Navigation (15:36) Specter's path navigators double as a declarative query language when paired with `select`, avoiding nested `map` and `reduce` pipelines. When modifying data without computing a new value, the `setval` macro provides a direct replacement mechanism that eliminates the need to wrap static values in `(constantly ...)`. Navigators like `FIRST` also allow targeted updates to specific individual elements deep within nested sequences without affecting adjacent records. Beyond selecting existing elements, Specter can target empty structural positions to perform non-destructive insertions. Using positional navigators such as `BEFORE-ELEM`, `AFTER-ELEM`, and `(BEFORE-INDEX 1)`, developers can insert elements at the head, tail, or between specific indices of a vector without overwriting existing data. For data structures lacking a fixed schema, the `WALKER` navigator recursively traverses arbitrary tree structures, pulling out all leaf elements matching a predicate such as `number?` or `string?`. Bi-Directional Parsing for Leaf Values (34:24) Specter's navigation model extends into individual leaf elements like formatted strings via bi-directional transformation. When modifying substrings—such as truncating minutes in a `"10.35"` timestamp string to `"10.00"`—the library's `parser` navigator pairs a downward `parse` function with an upward `unparse` function. Using `clojure.string/split` and `clojure.string/join`, Specter splits the string into a vector during navigation, allows further navigators like `LAST` to target the minute component directly, replaces the target value via `setval`, and reconstructs the joined string automatically. While acknowledging that time string modification is a contrived scenario, the host emphasizes that bi-directional parsing enables developers to reshape data representations dynamically without losing surrounding structure. The presenter concludes that while Specter's path navigation requires an initial conceptual shift compared to Clojure's standard threading macros, it significantly reduces boilerplate and improves readability. The host recommends testing paths iteratively in the REPL using `select` before applying transformations, viewing Specter as an indispensable tool for everyday Clojure engineering.

精炼阅读摘要

本期节目深入解析了由 Nathan Marz 开发的 Clojure 数据操作库 Specter;主讲人指出,该库通过泛化路径导航与双向回放重建机制,有效弥补了 Clojure 在集合类型保持以及嵌套数据结构读写操作上的不足。 集合类型保持与嵌套结构痛点 (1:21) Clojure 在处理数据集合时存在两个显著的开发痛点。首先是集合类型的保持问题:使用标准的 higher-order function 如 map 处理 vector 时,返回值会退化为惰性序列(LazySeq);若改用 mapv,即使输入是 set 也会被强制转换为 vector。主讲人指出,为了通用地保留原始输入类型,开发者通常不得不编写繁琐的条件分支或类型转换逻辑。而 Specter 的 transform 函数配合 ALL 导航器,在对元素执行函数变换(如 inc)时,能够自动保留输入集合的原生数据类型,无论是 vector 还是 set 均无需额外配置。 另一个更普遍的痛点在于嵌套数据结构的修改。主讲人认为 Clojure 核心库擅长处理扁平集合,但面对嵌套结构时语法较为繁琐。例如在包含多个 hash map 的 vector 中仅对偶数值进行自增,纯 Clojure 实现需要编写样板辅助函数:结合 for 推导式解构键值对、通过 into {} 重新组装 map、编写条件判断函数,并使用 mapv 维持外层 vector。这种传统实现方式不仅冗长易错,还大幅降低了业务代码的可读性与维护性。 路径泛化与逆向回放重建机制 (7:58) Specter 将 Clojure 原生的 get-in 概念泛化为一套完备的路径导航语言。在原生 Clojure 中,get-in 仅支持基于关键字或索引的基础读取(如 [:a :b :c] 或 [0 :b :c]),无法支持复杂的模式匹配、条件筛选或原地更新。Specter 则允许开发者在路径向量中自由组合导航器与断言。例如通过 [ALL MAP-VALS even?],即可直接遍历外层 vector 中的每个 map、提取其所有 value、过滤出偶数并执行 inc 变换,用单行声明式代码取代了过去数十行的结构重组逻辑。 主讲人将 Specter 的底层执行流程拆解为路径下行、数据变换以及“回放重建”三个阶段。导航器首先逐层穿透数据并定位目标元素,变换函数完成计算后,Specter 会沿着记录的路径轨迹逆向“回放”,在重新组装各层结构的同时将更新后的值替换回原位,未匹配的数据则保持原样。主讲人坦承自己并未深入研读 Specter 的源代码,其原理推导主要来自 Nathan Marz 的公开演讲与文档;同时他强调,得益于底层的工程实现,Specter 的执行性能往往优于开发者手写的手工遍历代码。 查询、直接赋值与位置插入导航 (15:36) 除函数式更新外,Specter 还提供了 select、setval 以及位置导航器,覆盖查询、直接赋值与数据插入等场景。主讲人演示指出,使用 select 查询复杂嵌套数据时,开发者需严格区分导航目标是“单个元素”还是“元素序列”;例如在使用 (submap [:start :stop]) 截取特定键值时,若路径在 points 序列后遗漏了 ALL,查询就会报错。在值替换场景下,setval 宏允许开发者直接传入静态目标值,避免了使用 transform 时必须包裹 constantly 或匿名函数的冗余。 此外,Specter 具备对数据结构“空白位置”进行导航的独特能力。通过 FIRST 可以精准定位并替换序列中的首个元素,而利用 BEFORE-ELEM、AFTER-ELEM 以及 (BEFORE-INDEX 1) 等位置导航器,开发者可以在集合的头部、尾部或任意索引之间插入全新数据项,而不会覆盖现有元素。主讲人还展示了 WALKER 导航器,该功能无需预设路径架构,能够以递归树的形式遍历整套嵌套数据,配合 number? 或 string? 等谓词即可扁平化抽取出所有匹配的叶子节点。 基于 parser 的双向数据转换 (34:24) 针对嵌套结构内部特定标量(如字符串)的深层局部修改,Specter 引入了基于 parser 的双向转换机制。主讲人以将数据中的时间字符串 "10.35" 统一向下取整为 "10.00" 为例,演示了如何突破基本数据类型的导航边界。该机制依赖一对配对函数:parse 函数负责在向下导航时将原始字符串拆解为可继续导航的结构(例如利用 clojure.string/split 拆分为 ["10" "35"]),而 unparse 函数则利用 clojure.string/join 定义逆向缝合逻辑。 在执行过程中,开发者在定位到 :stop-time 后插入 (parser parse unparse),随后即可继续使用 LAST 导航到分钟部分,并通过 setval 将其重置为 "00"。主讲人承认,通过解析字符串来修改时间是一个偏向演示性质的用例,但这清晰展现了 Specter 的扩展潜力:只要定义好正反向转换规则,开发者便能无缝操作深层异构数据,并完全依赖 Specter 的逆向回放机制无损恢复外层整体结构。 主讲人总结认为,Specter 凭借精简的导航抽象与高效的回放重建机制,显著改善了 Clojure 在处理复杂数据时的开发体验;但他也指出,这种路径导航思维初学者需要一定时间适应,且在构建复杂查询与双向解析时,开发者必须清晰分辨当前路径是指向单一元素还是元素序列。

Detailed notes — zh

## Background 该视频由 YouTube 频道 Misophistful 于 2017 年 10 月 6 日发布。视频简介指出,本期内容旨在帮助开发者学习如何使用 Nathan Marz 开发的 Specter 库优雅地操作 Clojure 中的嵌套数据结构,并提供了相关工具配置与项目链接。 ## TL;DR 主讲人针对 Clojure 原生高阶函数无法保留数据类型以及处理深层嵌套数据结构繁琐脆弱的痛点,系统介绍了 Specter 这一数据操作库。内容详细拆解了 Specter 沿路径下钻导航再反向回溯重建的核心机制,并对比了原生 Clojure 代码与 Specter 的简洁表达。随后,主讲人通过真实复杂的 JSON 嵌套数据演示了 select 查询、transform 就地重排、setval 绝对值赋予、元素间隙插入以及 walker 深度递归扫描等进阶技巧。最后,视频通过构建互逆的 parse 与 unparse 函数配合 parser 导航器,展示了深入字符串片段进行结构变换的强大能力。 ## Atmosphere & style 整个视频洋溢着主讲人对优秀软件工程纯粹而狂热的赞赏与推崇之情。讲解风格沉稳、透彻且极富感染力,主讲人在 Atom 编辑器与 REPL 环境中边写边测,遇到报错时从容剖析维度陷阱并逐步推导修正。观众在跟随其步步拆解复杂嵌套数据的过程中,不仅能清晰感知到代码量断崖式缩减的直观快感,更能真切体会到抽象设计与数据流转在 REPL 交互反馈下浑然天成的优雅与震撼。 ## Bulletin - [01:21](https://www.youtube.com/watch?v=rh5J4vacG98&t=81s) 针对 Clojure 原生高阶函数(如 map)破坏原有数据结构类型的痛点,Specter 通过 transform 函数实现无论输入 Vector 还是 Set 均原样保留数据结构类型,且具备匹敌手写 Clojure 代码的高性能。 - [09:26](https://www.youtube.com/watch?v=rh5J4vacG98&t=566s) Specter 将 Clojure 原生的 get-in 路径机制升级为通用导航语言,不仅能下钻并回溯重建嵌套不可变数据,还支持查询(select)、直接赋值(setval)与整树扫描(walker)。 - [27:37](https://www.youtube.com/watch?v=rh5J4vacG98&t=1657s) Specter 突破了传统键值遍历的限制,支持 BEFORE-ELEM、AFTER-ELEM 及 before-index 等位置导航器,实现了在不可变序列特定间隙进行原地插入操作的能力。 - [34:24](https://www.youtube.com/watch?v=rh5J4vacG98&t=2064s) 通过引入双向对称的 parser 导航器配合自定义的 parse 与 unparse 互逆函数,开发者可直接下钻并局部修改字符串片段,再由引擎反向自动重构完整数据。 ## Key takeaways - [01:21](https://www.youtube.com/watch?v=rh5J4vacG98&t=81s) Clojure 原生 map 和 filter 等高阶函数在操作 Vector 或 Set 时无法自动保留原有容器类型,导致开发者不得不编写充斥着类型还原或分支判断的样板代码。 - [03:29](https://www.youtube.com/watch?v=rh5J4vacG98&t=209s) 原生 Clojure 语法擅长处理扁平结构,但面对多层嵌套数据时,简单的条件修改需要嵌套 for 循环、into 还原及 mapv,逻辑脆弱且冗长。 - [07:58](https://www.youtube.com/watch?v=rh5J4vacG98&t=478s) Specter 的核心概念是“路径导航”(Path Navigation),将遍历、键取值与条件谓词组合为紧凑声明式路径,一行代码即可实现深层精准筛选与转换。 - [10:40](https://www.youtube.com/watch?v=rh5J4vacG98&t=640s) Specter 的运行机制分为下钻导航与回溯重建:沿路径向下定位目标并转换后,系统会反向“播放”路径,将改动无缝拼装回结构完全相同的不可变数据中。 - [13:00](https://www.youtube.com/watch?v=rh5J4vacG98&t=780s) 正确使用 Specter 的核心心智模型在于明确区分当前导航指针是指向“单个元素(Element)”还是“元素序列(Sequence)”。 - [14:57](https://www.youtube.com/watch?v=rh5J4vacG98&t=897s) Specter 在提供高级抽象的同时具有极其优异的底层性能,即使是精心手写的原生 Clojure 数据操作代码也很难在性能上超越它。 - [16:20](https://www.youtube.com/watch?v=rh5J4vacG98&t=980s) 除了修改数据的 transform 外,Specter 还提供了 select 查询函数,采用完全一致的路径语法提取深层嵌套字段。 - [23:26](https://www.youtube.com/watch?v=rh5J4vacG98&t=1406s) 对于无需关心旧值的覆盖操作,setval 提供了比 transform 配合 constantly 更加简洁直接的一等公民赋法语义。 - [27:37](https://www.youtube.com/watch?v=rh5J4vacG98&t=1657s) Specter 不仅能定位具体元素,还能精确定位到元素前后的“空间位置”(如 BEFORE-ELEM 与 AFTER-ELEM),在不可变集合中实现原地插入效果。 - [30:15](https://www.youtube.com/watch?v=rh5J4vacG98&t=1815s) 通过 before-index 导航器,开发者可以定位到指定下标之间的空白间隙,清晰区分了“修改指定索引元素”与“在指定索引处插入新元素”。 - [32:44](https://www.youtube.com/watch?v=rh5J4vacG98&t=1964s) 内置的 walker 导航器支持对复杂树形结构进行深度递归扫描,无需指定具体访问路径即可一键抽取或修改所有符合断言的叶子节点。 - [37:03](https://www.youtube.com/watch?v=rh5J4vacG98&t=2223s) 利用 parser 导航器挂载互逆的 parse 与 unparse 函数,Specter 能深入字符串等非集合类型内部,就地修改局部片段并自动回溯还原完整文本。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=rh5J4vacG98&t=0s) 认识 Specter:Clojure 的核心拼图 主讲人向大家介绍了由 Nathan Marz 开发的 Clojure 数据操作库——Specter。主讲人毫不掩饰对这个库的喜爱,称其为自己最常用的 Clojure 库之一,甚至大胆地宣称: > “I want to take a little bit of your time to explain why it's one of my favorite Clojure libraries, why I use it all the time, and even why I think it's one of the best pieces of software engineering ever produced.” 这期视频将分为三个核心部分: 1. **为什么选择 Specter(Why Specter)**:剖析为什么开发者应当关注并学习这个库。 2. **它是如何工作的(How it works)**:基于主讲人的理解,深入解释其底层运行机制。 3. **实际案例演练(Examples)**:演示 Specter 的具体用法及其强大能力。 该视频的受众定位主要是已经掌握 Clojure 基础、但尚未深入研究 Specter 的开发者。主讲人坦言,如果你已经是 Specter 专家,可能学不到太多新东西;而对于完全没有 Clojure 经验的纯新手,这或许不是最温和的入门教程,但仍能借此一窥该语言在强大库辅助下所能达到的高级形态。 --- ### [01:21](https://www.youtube.com/watch?v=rh5J4vacG98&t=81s) Clojure 的首个痛点:高阶函数无法保留数据结构类型 Clojure 本身设计精美、缺点极少,但在处理某些数据时会带来痛苦。主讲人指出的第一个痛点是:Clojure 在使用 `map` 或 `filter` 等高阶函数时,很难维持原有的数据结构类型。 例如,当开发者对一个向量(Vector)应用 `map` 时,返回的实际上是一个惰性序列(Lazy Sequence)或列表,而不是传入的向量。虽然 Rich Hickey 做出这样的语言设计有其深层考量,但在日常编码中,这往往违背了开发者的直觉:如果一个函数的入参是向量,出参通常也期望是向量。虽然可以使用 `mapv` 显式保留向量类型,但这种做法缺乏泛型通用性——一旦传入的是集合(Set),`mapv` 依然会强行返回向量,而非集合;要处理不同类型,代码中就不得不充斥着各种类型判断与条件分支。 Specter 则采用了完全不同的解决思路。通过其核心函数 `transform`,Specter 会默认保留输入数据结构的原始类型: ```mermaid flowchart TD accTitle: 高阶函数与 Specter 在数据类型保持上的对比 accDescr: 展示 Clojure 原生 map 和 mapv 与 Specter transform 在处理不同数据结构类型时的输出差异 subgraph Native["Clojure 内置高阶函数"] A1["Vector: [1 2 3 4]"] -->|"map inc"| B1["LazySeq: (2 3 4 5)"] A2["Set: #{1 2 3 4}"] -->|"mapv inc"| B2["Vector: [2 3 4 5]"] end subgraph SpecterEngine["Specter transform"] C1["Vector: [1 2 3 4]"] -->|"transform [ALL] inc"| D1["Vector: [2 3 4 5]"] C2["Set: #{1 2 3 4}"] -->|"transform [ALL] inc"| D2["Set: #{2 3 4 5}"] end ``` 在调用 `(transform ALL inc data)` 时,传入 Set 就会原样返回 Set,传入 Vector 就会返回 Vector。保持结构类型只是 Specter 的“副手绝活”,它的真正杀手锏在于对嵌套数据结构的优雅操作。 --- ### [03:29](https://www.youtube.com/watch?v=rh5J4vacG98&t=209s) 原生 Clojure 处理嵌套数据结构的噩梦 嵌套数据结构在 Clojure 中极其常见,其语法支持开发者极其快速、轻松地创建多层嵌套。然而,Clojure 核心库和默认语法只擅长处理扁平或单层结构,一旦面临嵌套,数据结构的遍历与维护就会迅速变成一场灾难。 为了说明这一点,主讲人给出了一个非常简单的嵌套结构:一个包含三个 Hash Map 的向量(`[{:a 1,:b 2} {:c 3,:d 4}...]`),Map 中的键为 Keyword,值为整数。假设需求是:**遍历该结构,仅将所有偶数递增(increment),其余元素保持原样不变**。 在原生 Clojure 中,没有现成函数可以直接完成该操作。开发者不得不手动拼装辅助逻辑: 1. 先定义一个通用的辅助函数 `apply-function-to-hash-map`,利用 `for` 循环解构出键值对,保留键并对值调用目标函数,由于 `for` 返回惰性序列,最后还要用 `into {}` 倒回 Map 中。 2. 定义谓词转换函数 `inc-even`:如果传入数字是偶数则调用 `inc`,否则直接返回原数字。 3. 外部层级使用 `mapv` 遍历外层向量,并嵌套调用写好的 Map 转换逻辑。 ```clojure ;; 原生 Clojure 的繁琐实现 (defn apply-function-to-hash-map [f m] (into {} (for [[k v] m] [k (f v)]))) (defn inc-even [n] (if (even? n) (inc n) n)) (mapv #(apply-function-to-hash-map inc-even %) data) ``` 不仅代码行数冗长,充斥着丑陋的 `for` 循环与显式的类型还原操作,而且逻辑极其脆弱、不易扩展。 --- ### [07:58](https://www.youtube.com/watch?v=rh5J4vacG98&t=478s) Specter 的路径导航与优雅直觉 面对完全相同的需求,Specter 的写法极其简洁。开发者只需使用 `transform` 函数,并提供一条导航路径(Navigator / Path): ```clojure (transform [ALL MAP-VALS even?] inc data) ``` 这行代码直接表达了业务意图:遍历向量中的所有元素(`ALL`),深入每个 Map 的所有值(`MAP-VALS`),仅筛选出满足偶数条件的值(`even?`),最后执行 `inc` 变换。数据结构的外层向量和内层 Map 结构被完整保留,偶数被正确递增,代码量断崖式下降。 主讲人指出,Nathan Marz 提出的这种“导航(Navigation)”抽象,正是初学者容易望而却步的地方: > “I think this is the thing that might put people off Specter in the beginning because it's kind of different to other ways that you've been used to with Clojure and I know for me it took me a while to grok it, to understand it, but once you do get it you realize how simple and powerful a way of describing navigating around the data it really is and you really fall in love with it.” --- ### [09:26](https://www.youtube.com/watch?v=rh5J4vacG98&t=566s) 概念锚定:从 Clojure 原生的 `get-in` 理解导航 为了帮助大家更好地理解这套机制,主讲人试图将其与 Clojure 原生生态中已有的概念进行类比。最贴切的参照物就是核心库中的 `get-in`。 在 Clojure 中,当开发者需要访问多层嵌套 Map(例如 `{:a {:b {:c 1}}}`)的最深层值时,不需要一层层显式解构,而是向 `get-in` 传入一个路径向量: - `(get-in data [:a:b:c])` - 路径向量告诉程序:首先找到键 `:a` 对应的值,接着在里面找到键 `:b` 对应的值,最后定位到键 `:c`。 Specter 的路径设计正是对这种“路径导航”思想的高度泛化与升级。`get-in` 仅支持静态键查找,而 Specter 的路径则可以将结构展开、集合遍历、条件过滤和位置定位融合在一起,形成一套通用且强大的数据导航语言。 ### [10:40](https://www.youtube.com/watch?v=rh5J4vacG98&t=640s) 从 Clojure 原生导航到 Specter 的路径下行与回溯重建 主讲人首先展示了 Clojure 内置函数 `get-in` 的工作机制:通过提供一个由关键字(keyword)或索引组成的路径向量(如在包含两项的 vector 中使用索引 `0`,后接 `:b`、`:c`),可以成功下钻并返回值 `1`。然而,主讲人指出这只是 Specter 导航器(navigator)概念的最基础雏形,因为 `get-in` 无法灵活遍历所有数据结构。随后,他通过一个具体案例拆解了 Specter 中 `transform` 的完整执行流程:从一个包含多个 map 的 vector(如 `[{:a 1:b 2} {:c 3} {:d 4}]`)开始,Specter 沿着导航路径下行。首先通过 `ALL` 剥离外层 vector,直接定位到三个独立的 map;接着使用 `MAP-VALS` 导航到所有 map 的值 `(1 2 3 4)`;再通过断言函数 `even?` 进行过滤,只留下偶数 `(2 4)`。导航至路径终点后,转换函数 `inc` 被施加在这些目标值上,使其分别递增为 `3` 和 `5`。 ```mermaid flowchart TD accTitle: Specter 转换与重建执行流程 accDescr: 展示从下钻导航、函数转换到逆向回溯重建原始数据结构的过程。 subgraph Navigation ["下钻导航阶段 (Path Traversal)"] A["初始数据结构: Vector of Maps"] --> B["ALL: 导航到各独立 Map"] B --> C["MAP-VALS: 提取所有数值 (1, 2, 3, 4)"] C --> D["even?: 过滤偶数目标 (2, 4)"] end subgraph Transform ["变换阶段 (Transformation)"] D --> E["inc: 目标值递增为 (3, 5)"] end subgraph Reconstruction ["回溯重建阶段 (Reconstruction)"] E --> F["还原谓词过滤: 保留未修改的 1 和 3 -> (1, 3, 3, 5)"] F --> G["逆向还原 MAP-VALS: 重新组装回各个 Map"] G --> H["逆向还原 ALL: 重新封装回顶层 Vector"] end ``` 在转换完成之后,Specter 进入了最关键的“重建阶段”(reconstruction phase)。主讲人坦言自己并未深入翻阅 Specter 的底层源代码,这一理解来自作者 Nathan Marz 的演讲视频与周边资料:“what it does is it just plays what it it's recorded in terms of the path backwards to reconstruct back to where you were but along the way it replaces the values that you've updated with your transformation function”(它所做的就是将记录下的路径反向播放,一路重建回你开始的地方,并在此过程中用变换函数更新后的值替换原来的值)。它记住哪些元素匹配了 `even?` 过滤并做局部替换,再通过反向操作 `MAP-VALS` 和 `ALL`,逐层将结构原样拼装回去,生成结构完全一致、仅目标值改变的新不可变数据结构。 - 导航的一个核心心智模型是区分目标:每一次在数据结构中导航,既可以导航至单个元素(element),也可以导航至多个元素构成的序列(sequence)。 - Specter 的 `ALL` 导航器会聚焦到集合内部的每一个元素,但在逻辑上让它们保持独立,而不是将它们包裹在新的临时容器中。 --- ### [14:57](https://www.youtube.com/watch?v=rh5J4vacG98&t=897s) 架构赞誉与真实 JSON 嵌套数据的 select 查询 主讲人强调了 Specter 令人惊叹的工程实现,直言它是自己最喜欢的软件工程杰作之一:“even hand-rolled Clojure code struggles to beat the performance characteristics of Specter”(即便手写原生的 Clojure 代码,也很难在性能表现上击败 Specter)。Specter 不仅提供了高度通用的路径抽象,还能保证在返回相同数据结构的同时具备极高的执行效率。接着,他指出导航路径不仅能用于数据更新,还可以直接停在导航终点作为查询机制。为了展示这一点,他从网上复制了一段真实的 JSON 嵌套数据(经过 Clojure 数据结构化):外层 map 包含元数据与一个 `:res` 键,其值是一个 vector,里面嵌套多层 map 与 vector(涵盖 `:cat-list`、`:points` 等)。如果使用 Clojure 默认的嵌套 `map` 配合匿名函数处理,代码会极其冗长臃肿,而 Specter 能以相同的优雅度应对复杂的深度嵌套。 Specter 提供了名为 `select` 的查询函数,其命名与 SQL 保持一致,接收与 `transform` 相同的路径参数。当目标是获取所有底层的起始与结束时间(`:start` 和 `:stop`)时,主讲人最初构建的路径为 `[:res ALL:cat-list ALL:points]`。执行后虽然成功获取到了包含 `:start`、`:stop` 和 `:point` 的数据,但还未完全达到只保留起止时间的目的。 - 主讲人极力推崇 Specter 的泛化抽象能力与近乎极客级别的性能优化,这也是他在实际项目中大量使用 Specter 的根本原因。 - 深度嵌套的真实数据中充斥着多层 vector 与 map 的交替嵌套,使用 Clojure 原生方法需要编写复杂的嵌套 map/reduce 管道。 --- ### [18:19](https://www.youtube.com/watch?v=rh5J4vacG98&t=1099s) submap 过滤的维度陷阱与 transform 深度排序 为了在查询结果中仅保留 `:start` 和 `:stop`,主讲人尝试引入内置的 `submap` 导航器,最初写成 `[...:points (submap [:start:stop])]`,但控制台直接报错。他随即意识到问题所在,重新呼应了之前强调的核心概念:必须清晰跟踪当前导航的是“单个元素”还是“元素序列”。由于 `:points` 对应的是一个包含多个 map 的 vector,直接对其应用 `submap` 会因类型不匹配而失败;必须在中间追加一个 `ALL`,即 `[:res ALL:cat-list ALL:points ALL (submap [:start:stop])]`,代码随即顺利运行,精准返回了仅含 `:start` 与 `:stop` 键的 map 列表。 展示完查询后,主讲人进一步演示在多层嵌套结构中使用 `transform`。他设定了一个需求:深入到数据结构最底层的 `:points` 序列,按照每个点的 `:stop` 字段对这些 points 进行逆向排序(reverse sorting)。他将路径定位到 `[:res ALL:cat-list ALL:points]`,并在转换函数中应用 `#(reverse (sort-by:stop %))`。在 Atom 编辑器中逐步折叠展开、核对复杂的嵌套返回值后,他确认对应分类下的数据点成功发生了位置置换:“see now point two comes first and point one comes second”(看,现在 point 2 排在第一位,而 point 1 排在第二位了)。整个外层数据架构完全保持原样不变,唯独深度嵌套中的 points 数组在原地完成了重排。 - 使用 `submap` 时,需要确保当前指针处于单个 map 元素上;若处于包含多个 map 的 vector 上,必须先接入 `ALL` 进行拆解。 - 对嵌套序列整体进行操作(例如调用 `sort-by`)时,路径应停留在序列本身(即指向 `:points`),而不需要再添加 `ALL`,以便将整个 vector 传递给转换函数。 ### [02:02:09](https://www.youtube.com/watch?v=rh5J4vacG98&t=7329s) 导航至序列与就地排序:Specter 的简洁与表达力 讲者回顾了对嵌套数据结构中 `:points` 向量进行排序的操作。导航路径定位到该层级后,将指向的数据结构——即由多个 Map 组成的向量(vector of maps)——直接传入转换函数中。该函数调用 `sort-by` 按照 `:stop` 时间进行排序,并使用 `reverse` 反转顺序,从而实现逆序排列。整个转换过程极其紧凑,只需三行代码(若不换行甚至只有一行),却完成了一次跨越多层嵌套的数据重排,并且完全保持了其余外层结构的不可变性与完整性。 讲者对此由衷赞叹:“And as you can see, you can do something incredibly powerful with very little code that once you get used to reading it is really highly readable.” 他强调,一旦掌握了 Specter 的语法和导航思维,读者很快就能一眼看懂数据是如何被定位和穿透的。定位完成后,开发者只需要清楚当前所在的位置到底是一个序列还是一个单一元素,然后将转换函数应用其上,即可直接拿到更新后的结构,没有任何多余的样板代码。这也是为什么 Specter 如此令人惊艳、并在实际开发中被高度重用的核心原因。 --- ### [02:23:26](https://www.youtube.com/watch?v=rh5J4vacG98&t=8606s) 用 `setval` 替代 `transform` 赋予绝对值 在数据操作中,除了根据旧值生成新值外,另一种极常见的需求是直接用新值覆盖目标位置,而根本不关心旧值是什么。若使用 `transform` 函数实现替换,开发者必须传递一个忽略参数的匿名函数,或者借助 Clojure 内置的 `constantly`,例如写成 `(transform [:timestamp] (constantly 12345) data)`。为了免去这种冗余包装,Specter 的作者 Nathan Marz 提供了专门的辅助函数 `setval`。 讲者以修改顶层的 `:timestamp` 为例演示了这一用法:直接调用 `(setval [:timestamp] 12345 data)`,顶层的时间戳便被立即更新,而其余数据结构完好无损。`setval` 的便利之处在于允许开发者省去 `constantly`,直接填入目标值。这种模式既适用于简单标量属性的重写,也同样适用于深层嵌套集合内部特定元素的替换。 ```mermaid flowchart LR accTitle: setval vs transform replace flow accDescr: 比较使用 setval 和使用 transform 配合 constantly 替换数据值的过程。 subgraph TransformWay["transform 方式"] direction TB T_Path["导航到目标路径"] --> T_Func["传入 (constantly new-val)"] T_Func --> T_Res["替换为新值"] end subgraph SetvalWay["setval 方式(推荐)"] direction TB S_Path["导航到目标路径"] --> S_Direct["直接传入 new-val"] S_Direct --> S_Res["替换为新值"] end ``` - 使用 `transform` 覆写值时必须使用类似于 `(constantly val)` 或 `(fn [_] val)` 的形式。 - `setval` 是对覆写操作的一等公民封装,语法签名更加直观:`(setval path val data)`。 --- ### [02:25:23](https://www.youtube.com/watch?v=rh5J4vacG98&t=8723s) 精确导航与单元素替换:`FIRST` 导航器的组合 讲者演示了如何利用 Specter 的内置导航器精准下钻并替换嵌套集合内部的特定元素。假设需要替换 `:res` 中第一个 group、其内部 `:cat-list` 中第一个元素、再其内部 `:points` 向量中的第一个点。此时不再使用通配的 `ALL`,而是换用专门定位单个元素的导航器 `FIRST`。 导航路径构建为 `[:res FIRST:cat-list FIRST:points FIRST]`。配合 `setval`,讲者将原本包含 `:start` 和 `:stop` 的完整 Map 替换为一个全新的简单 Map(命名为 `{:point "X"}`)。运行后 REPL 返回的数据结构显示,整个嵌套树其余部分毫秒未改,唯独最底层的第一个点被替换为了全新的结构。讲者解释道,正因为导航器精确锁定了“单一元素”(element),哪怕赋给它的是一个新的数据结构(如 Map),Specter 也会将其作为该位置上的单个元素进行整体替换。 --- ### [02:27:37](https://www.youtube.com/watch?v=rh5J4vacG98&t=8857s) 元素位置插入:`BEFORE-ELEM` 与 `AFTER-ELEM` 除了定位并修改已有元素,Specter 还支持向集合内“插入”新数据。讲者指出,Specter 不仅能导航到特定元素或序列,更具备定位到数据结构中“特定空间位置”(position)的独特能力。通过定位到某个元素的前后空隙,配合 `setval` 就可以实现类似于 `insert` 的效果,在不破坏现有数据的情况下插入新元素。 为了演示在 `:point-2` 之后插入一个新点 `:point-Z`,讲者引入了导航器 `AFTER-ELEM`(以及对应的 `BEFORE-ELEM`)。导航路径走向目标向量后,加上 `AFTER-ELEM` 即可锚定到序列末尾或指定元素之后的位置;执行 `setval` 赋入 `{:point "Z"}` 后,原本只有两个元素的 `:points` 向量顺利扩展为三个元素。讲者接着把导航器改为 `BEFORE-ELEM` 进行了验证,新元素随即准确插入到该序列的最前端。讲者感叹道:“Specter has an ability not just to navigate to a specific element or a specific sequence within a data structure, but can actually navigate to a specific position.” 这种面向位置而非单纯面向键值的导航机制,极大地增强了对不可变序列的编辑能力。 --- ### [02:30:15](https://www.youtube.com/watch?v=rh5J4vacG98&t=9015s) 索引与间隙导航:`nthpath` 与 `before-index` 在继续探讨精准插入时,讲者提到了基于下标的导航。首先尝试的是宏函数 `nthpath`(如 `(nthpath 1)`),但他很快在 REPL 中辨析了其作用:`nthpath` 本质上是导航到集合中的指定**元素**本身(通过 0 起始的索引访问第 N 个元素),因此对其调用 `setval` 会触发替换而非插入,相当于指定索引版的 `first` 或 `last`。 为了找到真正的“索引间隙插入”,讲者借机展示了 Specter 详尽的 GitHub Wiki 文档。文档被清晰地划分为两大部分:**Navigators**(导航器)与 **Macros**(宏)。在导航器列表中,讲者查找到了目标导航器 `before-index`。根据文档定义,`before-index` 正是用于导航到某个索引与其前一个索引之间的“空白位置”(empty space)。讲者将代码修改为 `(before-index 1)` 并重新运行,新元素被精准地插在了索引 0 和 1 之间的夹缝中。这一实验彻底厘清了“导航到具体元素”与“导航到位置/索引间隙”之间的关键语义区别。 | 导航器 | 导航目标性质 | 配合 `setval` 的效果 | 典型应用场景 | |:--- |:--- |:--- |:--- | | `FIRST` / `LAST` / `(nthpath n)` | 集合中的**现有元素** | 替换该位置上的现有元素 | 修改特定下标的记录 | | `BEFORE-ELEM` / `AFTER-ELEM` | 集合元素的**前端/后端位置** | 在该端点追加/前置新元素 | 队列头部推入或末尾追加 | | `(before-index n)` | 索引 $n$ 与 $n-1$ 之间的**空白间隙** | 就地插入新元素,后续元素后移 | 在列表指定中间位置插入数据 | --- ### [02:32:44](https://www.youtube.com/watch?v=rh5J4vacG98&t=9164s) 深度递归扫描:使用 `walker` 遍历任意叶子节点 除了沿着固定路径下钻,Specter 还提供了极其强大的整树扫描能力——内置的 `walker` 导航器。深层嵌套的数据结构本质上是一棵树,而现实需求往往是在不知道具体键路径的情况下,提取或修改整棵树上所有满足条件的叶子节点。 讲者演示了将 `select` 查询与 `walker` 结合使用的技巧。通过传入断言 `(walker number?)`,Specter 遍历了传入数据的所有分支与层级,仅在命中数值型叶子节点时停下,并将整棵树中所有分散的数字统一收集为一个序列返回。讲者总结道:“Walker is a fantastic way to navigate to all of the nested leaves of any deeply nested data structure which tends to look like a tree.” 接着他将断言替换为 `string?`,同样瞬间从同一复杂结构中抽取出所有的字符串值。最后,讲者抛出下一个进阶场景的引子:如果在导航过程中需要对字符串数据进行就地解析(parse)和切割变换,又该如何借助 Specter 的机制优雅达成。 ### [34:24](https://www.youtube.com/watch?v=rh5J4vacG98&t=2064s) 深入字符串内部:定义互逆的 parse 与 unparse 函数 在对数据结构进行导航时,有时需要进一步深入到字符串本身的某个局部片段进行精确修改。主讲人提出了一个新的操作目标:“将所有 stop-time 向下取整到最接近的整点小时”(例如把 `"10.35"` 变成 `"10.00"`)。在 Specter 中实现这一需求的核心机制,是提供一对互逆的函数:`parse` 函数与 `unparse` 函数。主讲人解释了这种双向对称设计背后的原理:“正如我之前解释过的,Specter 的工作机制是先沿着路径一路向下导航,然后再原路重构回去;因此当你向下导航并执行 parse 时,它会调用 parse 函数按照你的意愿拆分数据以供继续操作,但在沿反方向重构数据时,它需要 unparse 函数把它们重新拼合起来。” 为了处理用点号分隔的时间字符串,主讲人开始编写这对辅助函数。首先是解析函数 `parse`,它接收一个时间字符串,使用 `clojure.string/split` 进行切分。由于点号在正则表达式中是匹配任意字符的保留关键字,因此必须使用反斜杠对其转义(`\.`)。在 REPL 中测试 `(parse "10.35")` 后,成功返回了包含两个字符串的向量 `["10" "35"]`。接下来编写对应的逆向函数 `unparse`,它接收拆分后的字符串向量,调用 `clojure.string/join` 并以点号作为分隔符进行拼接。测试传入 `["10" "35"]` 后正确还原为 `"10.35"`,确保了这两个函数具备完全对等的互逆行为。 ```mermaid flowchart LR accTitle: parse与unparse双向转换流程 accDescr: 展示字符串与向量之间通过parse向下拆分与unparse向上重构的双向映射关系。 A["原始时间字符串\n('10.35')"] -->|"向下导航: parse\n(clojure.string/split)"| B["拆分后的向量\n(['10', '35'])"] B -->|"向后重构: unparse\n(clojure.string/join)"| A ``` - 解析操作依赖 `clojure.string/split`,必须转义正则表达式中的点号保留字符(`\.`)。 - 还原操作依赖 `clojure.string/join`,参数顺序为分隔符在前、集合在后。 - 在 REPL 中验证通过:`parse` 将 `"10.35"` 拆分为 `["10" "35"]`,`unparse` 将 `["10" "35"]` 重新还原为 `"10.35"`。 ### [37:03](https://www.youtube.com/watch?v=rh5J4vacG98&t=2223s) 利用 `parser` 导航器进行结构变换与赋值 定义好解析与逆解析逻辑后,主讲人展示了如何在导航路径中挂载它们。目标是将匹配到的时间分段替换为取整后的数值,这里使用的是便捷变换函数 `setval`。为了逐步验证路径是否正确,主讲人分享了一个实战调试技巧:“在构建转换逻辑或定位值时,你随时可以先使用 `select` 来查看当前导航到了什么位置。”他先将路径导航至 `:points` 下的所有 map,再深入到 `:stop-time` 字段,通过 `select` 确认成功拿到了底层的 stop-time 字符串列表。 随后,主讲人引入了 Specter 内置的导航器函数 `parser`,它接收先前定义好的 `parse` 和 `unparse` 作为参数。挂载 `(parser parse unparse)` 后,数据在 Specter 的视角中动态转化成了拆分后的向量集合。主讲人指出,尽管在普通的变换函数内部也能手动完成解析与拼接,但将其提升为导航器的一部分具有不可替代的优势:“在这里我们可以继续借助导航器的威力。”这意味着可以直接在解析后的向量上追加 Specter 内置的导航关键字 `LAST`,精准定位到分钟字段。接着将操作切换回 `setval` 并把目标值设为 `"00"`,原本的 `"1.35"` 和 `"2.35"` 便在重构后干净利落地更新为 `"1.00"` 与 `"2.00"`。 ```mermaid flowchart TD accTitle: Specter parser导航路径拆解 accDescr: 描述从外层数据结构一路导航经过parser并最终使用LAST修改字段值的层级流转。 subgraph S1["定位原始字段"] D1["根数据结构"] --> D2[":points 集合"] D2 --> D3[":stop-time 字符串\n('1.35', '2.35')"] end subgraph S2["动态结构映射与修改"] D3 -->|"挂载 parser\n(parse + unparse)"| D4["拆分向量\n(['1', '35'], ['2', '35'])"] D4 -->|"导航器 LAST"| D5["分钟片段\n'35'"] D5 -->|"setval 设为 '00'"| D6["修改并重构为 '1.00', '2.00'"] end ``` - 导航路径层层下钻:从顶层进入 `:points`、遍历集合,定位到 `:stop-time`,再经由 `(parser parse unparse)` 将字符串暴露为向量。 - 利用 Specter 核心导航器 `LAST`,无缝指向向量的最后一项(即分钟部分)。 - 调用 `setval` 将定位到的分钟统一覆盖为 `"00"`,验证结果显示数据全部按预期向下取整。 ### [39:44](https://www.youtube.com/watch?v=rh5J4vacG98&t=2384s) 总结:Specter 的魔力与 Clojure 实战价值 完成修改演示后,主讲人坦言虽然这个向下取整的例子本身带有一点人为设计的痕迹,但它极佳地展示了如何通过自定义函数改变导航器观察数据的方式。主讲人总结了 Specter 最具吸引力的核心特质:“正是因为 Specter 能够神乎其技地在幕后为你把所有数据重新组装回去,你才能用极少的代码实现极其强大的效果。” 在全篇分享的尾声,主讲人毫不掩饰自己对该库的热爱与推崇:“正如大家所感受到的,我对 Specter 感到非常兴奋,在我用 Clojure 编写的所有项目中无时无刻不在使用它。”他表示希望通过这次对其核心原理、内部工作机制以及具体实战范例的系统性梳理,能够激发更多开发者的兴趣,促使大家亲自在项目中尝试 Specter,体会优雅操纵深层嵌套数据结构的便利。 ## Notable quotes > [14:57](https://www.youtube.com/watch?v=rh5J4vacG98&t=897s) even hand-rolled Clojure code struggles to beat the performance characteristics of Specter > [02:27:37](https://www.youtube.com/watch?v=rh5J4vacG98&t=8857s) Specter has an ability not just to navigate to a specific element or a specific sequence within a data structure, but can actually navigate to a specific position. > [02:32:44](https://www.youtube.com/watch?v=rh5J4vacG98&t=9164s) Walker is a fantastic way to navigate to all of the nested leaves of any deeply nested data structure which tends to look like a tree. ## Mentioned Specter, Clojure, Nathan Marz, Rich Hickey, Atom, proto-repl, JSON, SQL, GitHub ## Video description Learn how to elegantly manipulate nested data structures with Specter. Specter: https://github.com/nathanmarz/specter Language: http://clojure.org Editor: https://atom.io/ proto-repl: https://atom.io/packages/proto-repl proto-repl setup guide: https://gist.github.com/jasongilman/d... Atom syntax theme: https://atom.io/themes/borealis-syntax [Source](https://www.youtube.com/watch?v=rh5J4vacG98)

Source quotes

[14:57] even hand-rolled Clojure code struggles to beat the performance characteristics of Specter [147:37] Specter has an ability not just to navigate to a specific element or a specific sequence within a data structure, but can actually navigate to a specific position. [152:44] Walker is a fantastic way to navigate to all of the nested leaves of any deeply nested data structure which tends to look like a tree.

Original transcript

[0:00] hi everyone and welcome to a screencast [0:03] about understanding Spectre Spectre is a [0:06] data manipulation library for closure [0:08] written by Nathan Mars now I want to [0:10] take a little bit of your time to [0:12] explain why it's one of my favorite [0:13] closure libraries why I use it all the [0:16] time and even why I think it's one of [0:18] the best pieces of software engineering [0:19] ever produced B claims huh so let's dig [0:23] in and see if I'm right so let's start [0:26] by explaining what I'm going to be [0:27] talking about during this screencast I'm [0:29] gonna try and cover three main areas [0:31] first I'm gonna try and explain why [0:32] Spectre why should you be interested in [0:34] this library then I'm gonna try and [0:36] explain how it works based on the [0:38] understandings that I have and then I'm [0:40] gonna go through a few examples of what [0:42] you can do with it [0:43] and why it's so powerful I choose start [0:46] by saying that this this screencast is [0:49] aimed mainly at people who already [0:51] understand closure and presumably [0:54] haven't been delving too deeply into [0:56] Spectre yet if your respect to expert [0:59] there's a chance that you won't learn [1:00] too much new however if you haven't [1:02] really used back to before or you've [1:03] just looked briefly into it there's a [1:05] big chance that you might find something [1:07] new here for you and if you're brand new [1:09] to closure this might not be the best [1:10] starting video for you you might want to [1:12] go for a more gentle introduction video [1:14] but even for you you might be able to [1:16] see some of the more advanced things [1:17] that you can do with the language with [1:19] powerful libraries like this okay so [1:21] let's dig straight in so what are the [1:23] two main reasons why you might want to [1:24] start using Spectre so the first one and [1:28] both of these are related to weaknesses [1:30] that I think closure has and closure [1:32] doesn't have many weaknesses I think [1:34] it's a really beautifully designed [1:35] language but there are two areas where [1:37] it's a little bit painful to use closure [1:39] sometimes and the first one is that it [1:42] isn't very good at maintaining your data [1:43] structures type especially when you're [1:46] using higher-order functions like map or [1:48] filter let me do a quick example to [1:49] explain what I mean here so if we map a [1:51] function over a collection let's say we [1:54] have a vector that contains a few [1:55] numbers and we execute it what we get [1:58] back is actually a list or it's actually [2:00] a lazy sequence so it's not the vector [2:03] that we passed in and there are design [2:05] reasons as to why rich he went this way [2:07] and they make some sense but oftentimes [2:09] when you're writing your code you don't [2:12] want to lose that [2:12] summation if a vectors come in you want [2:15] a vector to go out of your function and [2:17] that's kind of tricky to do in closure [2:19] at least to be totally generic and cope [2:22] with any data structure type if we [2:24] wanted to just make sure that we kept [2:26] this as a vector of course we can use [2:27] map V but then we're explicitly saying [2:30] in our function we know the type is [2:32] coming in with forcing it to be this but [2:34] MATV won't work if we're passing in a [2:36] set for example instead then we're going [2:39] to get a vector out instead of the set [2:41] which is what we want so then maybe have [2:43] to put in a lot of conditionals or [2:44] handle it in some other way [2:46] but in any case it's not perfect however [2:49] Specter has a different approach and if [2:51] we use its transform function which is [2:53] the main function in spectrum I'll [2:55] explain more about it later but I just [2:57] want to show you how it differs and the [2:59] first argument is the Navigator again on [3:01] the get to that in due time and then you [3:04] tell the function and then you give the [3:06] date structure so let's say we let's [3:08] pass in a set to start with so 1 2 3 4 [3:11] so if we say transform all the elements [3:13] and increment them it's going to return [3:15] a set so by default Specter does what we [3:19] wanted it remembers the structure or the [3:21] type of your data structure and it [3:23] returns exactly the same ones which [3:25] we're passing the vector we get the same [3:26] result back and that's kind of a side [3:29] trick that the specter has its main act [3:32] as it were is what it what it's designed [3:34] to do what it's meant to do is to allow [3:36] you to elegantly manipulate your nested [3:38] data structures now because nested [3:41] structures are very common in closure [3:43] because it's part of the syntax to [3:45] quickly and easily create nested data [3:46] structures you're going to come across [3:48] these a lot but unfortunately the the [3:51] default syntax and core library [3:53] enclosure is not really elegant at [3:55] handling nested data structures it's [3:57] really good at dealing with ones that [3:59] just had one nested what you said [4:02] unmastered 8 structures as soon as you [4:04] even put in some kind of basic nesting [4:06] suddenly becomes a nightmare to maintain [4:08] the structure and should traverse over [4:10] it let me give you an example of what I [4:12] mean so let me paste in a date structure [4:15] that I made earlier and this is about as [4:18] simple as it gets when it comes to [4:19] nested data structures all we have is a [4:21] vector of Maps so it's a vector that [4:24] contains three maps and the maps just [4:26] contain [4:26] keywords that map to integers so it's [4:29] it's really as simple as it gets but [4:31] it's you know it's a normal date [4:33] structure that you might use in your [4:34] program let's say that we wanted to go [4:37] through this state structure and we want [4:39] to increment all of the even numbers [4:42] just those but leave the rest of the [4:44] date structure untouched how would we go [4:46] about doing that [4:47] how do we go about doing that with [4:50] standard closure code so there's not [4:53] actually a function enclosure for doing [4:56] that so let's have a look at how we [4:57] would write it ourselves so we would [5:00] define a function and let's call it [5:01] apply function to hash map that takes [5:08] two parameters one is the function and [5:10] one is the map and then what we can do [5:13] is we can do before and we can do [5:16] structure and we would take we know that [5:18] the hash map has sets of key value pairs [5:21] and then what we're going to do is we're [5:23] going to apply now we're going to return [5:30] so what we do so we we want to keep the [5:33] key and then we want to apply the [5:36] function to the value I think that's [5:39] right to apply function to hash map and [5:42] if we want to keep it as a map then [5:44] we're gonna once we've done that because [5:46] the for loop is going to return a lazy [5:48] sequence so we can pour that back into a [5:51] map I think that's right let's try that [5:54] quickly so apply function to hash map [6:00] and let's give it the function Inc and [6:02] let's give the hash map that's a one [6:05] okay we'll get a two back yes so that [6:08] seems to be working okay so if we want [6:12] to go across this nested data structure [6:15] where we have a vector of these then [6:16] we're gonna have to of course map this [6:18] function to map apply function in to [6:25] data I don't think we've actually [6:27] executed a traverse let's just put down [6:29] the ripple so what happens if we do this [6:31] map apply function hash map Inc across [6:34] data now that didn't work because of [6:37] course we need to give the [6:39] percentage and then we so instead of a1 [6:44] b2 [6:44] we've incremented all the values which [6:46] is kind of what we want but what we [6:48] actually want to do is only increment [6:50] the even values so now we're going to [6:52] have to define another helper function [6:54] let's call the Inc even it takes a [6:59] number and then let's say if even and [7:04] then we want to increment and otherwise [7:07] we can just return n okay so now we have [7:09] an Inc even and then let's go back and [7:11] let's mark apply function to hashmap and [7:16] this time instead of just Inc let's pass [7:18] our in even function centage on the data [7:23] and now hopefully it's gonna increment [7:25] only the even numbers let's run that yet [7:27] so 8:1 is left to is incremented to 3/3 [7:31] is left and 4 is incremented to 5 but [7:34] you'll see here because we've used map [7:36] we because of this problem number 1 [7:39] where it doesn't maintain the date [7:41] structure type we actually need to put [7:44] this back into being a vector so let's [7:45] reason Maithili instead of map and now [7:48] we get back exactly what we've put in [7:50] but with what we've applied but it's [7:52] kind of quite a lot of code and yeah [7:54] it's kind of a nasty 4 and I think we [7:58] can do that and that's where Spector [7:59] comes in so how would this look like [8:01] inspector well it's pretty simple we'll [8:04] use the transform function again and [8:06] this time we need to this time we need [8:09] to give what respect to refers to it as [8:13] a navigator or a path and then we can [8:16] put that inside of the vector and in [8:20] this case so we have a vector of map so [8:23] for all of the maps in the vector and [8:26] for all of the map vowels so all of the [8:29] values of each map then we only want the [8:32] even values so that that way we've [8:35] navigated through the date structure to [8:37] all the even values in all of the maps [8:39] and then we can apply ink on the data [8:43] now this returns exactly what we want [8:47] and as you see it's a lot less code than [8:48] doing it the manual closure way and I'm [8:51] going to explain how [8:52] navigator works and how you need to [8:54] think about this this abstraction that [8:57] Nathan's come up with about navigation [8:59] and I think this is the thing that might [9:01] put people off specter in the beginning [9:03] because it's kind of different to other [9:05] ways that you've been used to with [9:07] closure and I know for me it took me a [9:11] while to grok it to understand it but [9:13] once you do get it you realize how [9:15] simple and powerful a way of describing [9:17] navigating around the data it really is [9:19] and you really fall in love with it [9:21] so hopefully you'll get that epiphany [9:23] moment too okay so let's dig into how [9:26] this is actually working and what's [9:27] going on a little bit under the covers [9:30] now before I start explaining what's [9:32] going on I guess it could be useful to [9:34] try and anchor this in a concept that [9:37] you're already familiar with so for [9:39] example there is one function in closure [9:43] where this actually is kind of used in a [9:46] similar way let me is it get in get in [9:51] yeah this one so it firstly takes a map [9:54] so if we do get in a 1 and then we can [10:04] give it let's give some nested map so [10:09] let's say a 1 B C okay so now we have [10:18] three Maps nested in each other so the [10:20] value of a is another map and the key [10:23] key word for that is being the value of [10:26] the keyword is another map and that's C [10:28] so if we want to navigate straight to [10:30] the value of C with using get in we can [10:33] just give a vector that says first find [10:36] the value of a then find the value of B [10:38] and then find the value of C so that [10:40] should return one which it does so this [10:43] here is you can think that is a very [10:45] basic version of Spector's navigator [10:48] concept this navigates you through the [10:50] nested Maps but with getting it only [10:53] works with the keyword and I think it [10:58] also works with a sequence so you can [11:00] say which if one of these was a vector [11:02] let's see if that works I think this is [11:04] true [11:06] if we have a vector that has two [11:09] elements letting me foes there and then [11:15] we say because it's it will be zero [11:19] index so the first element then B then C [11:22] this should also return 1 if I this yes [11:24] so it works with both keywords and [11:28] sequential where it is in the vector the [11:30] the the sequence but that's kind of [11:33] basic and it doesn't really let you [11:35] navigate on all data structures in the [11:37] way but Specter allows you to do so if I [11:41] paste in an example I have from earlier [11:44] here's one I made earlier it tries to [11:46] walk through how this is working and [11:48] let's let's have a go and see if we can [11:50] figure out how transform and spectra is [11:52] working so going back to this example [11:54] where we have this data structure where [11:56] we've got a 1 B 2 in a map C 3 in a map [12:00] and d 4 in a map with wrapped up in a [12:02] vector so that's the starting date [12:04] structure so that's what's get passed in [12:07] here and then transform inspector is [12:11] running through this now look at the [12:13] sequence of navigations this path [12:15] through the data and first it says all [12:18] so what happens there is it kind of [12:20] strips away the vector and it navigates [12:22] you to all three maps and it keeps them [12:25] separate so it's kind of it's not useful [12:28] to imagine these living inside their own [12:30] vector and that's this is a key concept [12:33] I'm gonna come back to you later that [12:35] each time you navigate within a date [12:37] structure you can either navigate to an [12:39] element or you can navigate to a [12:41] sequence and in this case we're [12:43] navigating to multiple elements but not [12:45] a sequence so we've navigated to [12:48] multiple maps then what's the next thing [12:50] we navigate to then we say we want to [12:52] navigate to the map vowels so those are [12:54] the values for all of those maps so then [12:57] we navigate to again it's a collection [13:00] of elements so we have one two three and [13:03] four so all four values out of the maps [13:06] then we navigate to we've run like a [13:09] predicate function a filtering function [13:12] on this so we said now we only want to [13:14] see the ones that are even so it takes [13:16] away the one and the three and leaves us [13:17] with the two and the four and [13:19] then with our navigation is complete [13:20] we've gotten to the end of our path so [13:24] then what happens is Spectre applies the [13:26] function that you've told it to apply [13:28] the transformation function which is [13:30] inked so increments 2 to 3 and [13:32] increments 4 to 5 and then he goes into [13:36] its reconstruction phase and this is [13:37] where the real magic happens and this is [13:39] where I'm not 100% sure on how it works [13:41] because I've not actually looked into [13:43] the code it's more from my understanding [13:44] from Nathan's videos and reading around [13:47] but what I think happens is this what it [13:50] does is it just plays what it it's [13:53] recorded in terms of the path backwards [13:55] to reconstruct back to where you were [13:57] but along the way it replaces the values [14:00] that you've updated with your [14:02] transformation function so how would [14:04] that look in this case so if the first [14:07] thing is it remembers that it what the [14:09] data structure look like and that you [14:11] applied an even filtering so then what [14:13] you can do is go back to what it looked [14:15] like before that but remember which [14:17] one's match to the filtering and replace [14:19] only those so that gets you back to this [14:21] place where we have a 1 a 3 a 3 and a 5 [14:23] so it's only incremented the even [14:25] numbers then it does a reverse on the [14:27] map vowels so it reconstructs you back [14:30] to where you were where you actually had [14:31] the maps then it does a reverse on the [14:34] all and it gets you back to the date [14:36] structure that you started with so you [14:38] see how what it does is it kind of [14:39] navigates all the way down following [14:41] your path and then once it's got to the [14:43] bottom it applies the transformation and [14:44] then reconstructs on the way back to [14:46] give you exactly what you started with [14:48] but with the change functions it's kind [14:52] of difficult to understand but hopefully [14:54] hopefully I got some of the idea across [14:57] ok so it's doing this clever thing and [15:01] not only is it doing this but it's doing [15:03] it in with very effective code and this [15:05] is where my argument that this is one of [15:07] my favorite pieces of software [15:08] engineering ever comes from and that's [15:10] because even hand-rolled closure code [15:14] struggles to beat the performance [15:16] characteristics of Spector so not only [15:18] does it do with this generic abstraction [15:21] we have this path fist navigation [15:24] concept but it's only returning the same [15:27] date structure always but it does it [15:29] really really fast that's why I'm so [15:31] impressed with this and blown away [15:33] tend to use it a lot okay so what can [15:36] you actually do with it when would you [15:37] use this because it has this abstraction [15:42] of a navigator or a path you can [15:45] actually do query with that as well so [15:47] what I've shown you so far is [15:48] transformation so that's when you want [15:50] to change what's in a date structure [15:52] without affecting the rest of the date [15:54] structure but you can also just stop at [15:56] the navigation point and act as a query [15:59] so let's see how that might look so [16:02] let's let's paste in some semi-realistic [16:06] nested data that's a bit more nested and [16:09] nasty than just a vector of maps so [16:13] here's one I made earlier data let's [16:15] execute this so this is it's basically [16:19] some Jason you can imagine that's been [16:21] closure if I'd and so I actually got [16:24] this off a website so it's actually real [16:25] Jason from somewhere so it's a map that [16:29] has a couple of route elements and then [16:31] the res has a vector of other maps and [16:34] that has a vector of other maps which [16:36] also has a vector of other maps so it's [16:39] highly nested so this is not something [16:41] that you would want to be manipulating [16:43] with with this kind of default closure [16:47] code because you'll be writing something [16:49] very very convoluted with the seriously [16:52] nested the maps over matter the maps you [16:55] don't want to go there but here's where [16:57] the power of Spector comes in because he [16:59] can handle this just as easily as it can [17:01] handle the simple nested data structures [17:04] so let's have a look at what a query [17:07] might look like using a date structure [17:09] that looks like this okay so if we do so [17:13] what have I said here find all the start [17:15] and stop times [17:15] so let's write a query so query uses the [17:19] function called select which if you've [17:22] ever done SQL you'll be familiar with [17:23] the terminology and it works in just the [17:26] same way that you give a path a [17:28] navigator so we just need to figure out [17:32] if we want to find all the start and [17:34] stop times so they live down here right [17:36] at the bottom so we first want to [17:38] navigate to res then res is the vector [17:41] so then we want to navigate to all the [17:43] maps in that vector [17:44] then within that wind wanna navigate to [17:46] the catalyst and then we want to [17:49] navigate to all because catalyst is a [17:52] vector we want to navigate all the maps [17:54] there then what we want to grab our [17:57] other points let's just see what that [18:01] gives us so select rares all catalysts [18:04] all points from data okay so that's [18:07] navigated us down to the points so we [18:09] have start stop and point so I guess you [18:13] could say we're done here but what what [18:15] we actually wanted was just the start [18:17] and stop times not the points so is [18:19] there a way to do that with Specter and [18:22] and actually there is because within the [18:26] navigate within the path you can use [18:29] other functions including a built in one [18:32] that's called sub map I believe is [18:35] called yes Specter navigates to the [18:37] specified sub map so here we can say we [18:41] only want start and stop does that work [18:45] no that didn't work why didn't that work [18:52] yes I think I know why that didn't work [18:55] and this is what I was talking about [18:56] before about you need to keep track of [18:58] are you navigating to an element or to a [19:01] sequence of elements and because points [19:03] is actually a sequence of maps that was [19:05] fine before when we navigated there cuz [19:07] it just returned the sequence as part of [19:09] the result but now we want to do a sub [19:11] map on all of those maps so if we put [19:14] them all here hopefully this will work [19:16] yes that works perfectly so now we've [19:18] just gotten back the start and stop time [19:20] maps for all of the entries in this [19:23] highly nested data structure cool and [19:26] I've shown you what happens if you don't [19:29] keep track of if you're never getting to [19:30] an element or to a sequence which is [19:33] also very useful to know when you're [19:34] dealing with Specter okay so what else [19:37] can we do with Specter apart from [19:41] selection so a query well you've seen [19:44] update with transform but let's show you [19:49] on a more nested data structure how cool [19:52] and powerful that is so let's do the [19:54] same thing [19:57] is a transform and what should we [20:00] transform which I guess we could let's [20:04] change the order of the points based on [20:08] the stop time for example so we'll [20:11] navigate all the way down to the stop [20:13] time and we'll do a sorting based on a [20:18] reverse sorting based on the order so [20:20] let's see how that would look so this is [20:23] gonna be the same get all the way down [20:27] to the point okay so that's the density [20:31] navigation that's the path and now let's [20:34] just yeah and then what do we want to do [20:40] so we want to do we want to sort it by [20:44] the stuff time so let's just try doing [20:46] that if you just do a sort by stop oh [20:58] okay so let's touch the truck with a [21:01] highly nested a structure you have to go [21:03] down the next thing to see if what [21:05] you've expected to happen has happened [21:07] so cat one start 13 stop 1335 so 1335 14 [21:16] 35 so those have not been reversed oh [21:19] yeah because I actually haven't done the [21:21] reversing yet [21:23] I've only sorted them by stop time so [21:25] odd hang on are there any where they're [21:26] in the wrong time order right now ten [21:30] twelve no they're all in the right order [21:32] if we just changed this to be reverse [21:35] sort by stop now hopefully there will be [21:39] reversed if we drill all the way down [21:41] again the catalyst cat points yes [21:46] they've swapped over see see now point [21:48] two comes first and point one comes [21:50] second okay so let's go back and figure [21:54] out what we've actually done here [21:56] so we've navigated through the date [21:59] structure through all the way down to [22:02] the points so what what Specter is [22:06] seeing is a sequence of maps that are [22:09] the points [22:10] is looking at all of these in order and [22:14] then so the first one it comes to is [22:18] this so its navigated to here and then [22:21] we past that date structure that we've [22:23] navigated to so that vector of maps into [22:25] our function and our function takes that [22:30] and it does a sort by the stop time and [22:33] then reverses the order so sorts it by [22:36] stopped and then reverses yeah and [22:38] that's the data that we're passing in [22:40] and as you can see you can do something [22:43] incredibly powerful with very little [22:45] code that once you get used to reading [22:48] it is really highly readable because the [22:51] these navigations once you get used to [22:53] the syntax and how it works on what [22:55] you're looking for it doesn't take long [22:56] at all to realize how you've navigated [22:58] through the structure then once you're [23:00] there you need to remember where you are [23:02] are you at a sequence or an element the [23:04] here's the function you're applying and [23:06] then BAM you get exactly back what you [23:09] gave in with no extra code so it's still [23:11] just three lines of code or one line if [23:13] you weren't spacing it like this to do [23:15] something really kind of amazing so [23:18] hopefully you're starting to see now the [23:20] power of Specter and why I'm so [23:21] impressed with it and why I use it so [23:23] much but what are some other cases of [23:26] using this well there's a pretty common [23:28] case where instead of caring about where [23:32] you've navigated to in terms of what the [23:34] current value is so in this case we [23:36] cared because we wanted to keep the data [23:38] the same we just wanted to swap the [23:39] order sometimes you just want to [23:41] navigate in and replace a value so you [23:44] don't care about the value that's being [23:47] passed in so in this case it's the [23:48] percentage that's passed in and of [23:51] course you could do that with a [23:52] transform and just ignoring the [23:54] parameter but Nathan actually gave us a [23:57] nicer way of doing that he's got a [24:00] helper method called set valve that so [24:04] let's pick what should we know let's [24:06] just change the timestamp is then we [24:08] don't need to navigate all the way down [24:09] into that so let's say timestamp so [24:15] let's set our times down to just a [24:19] number on the data go and you see [24:23] straight away the time spot [24:24] stamp has been updated but the rest of [24:25] the date structure is left untouched and [24:28] that's instead of doing so how would [24:31] this look like if we use transform let's [24:33] just try that quickly we navigate to the [24:37] same place to time stamp but then we [24:40] can't just pass in a value would have to [24:42] have an anonymous function where we used [24:45] something like constantly [24:47] so let's yeah so that does exactly the [25:04] same but what's that bowel does it's [25:06] it's just the convenience function where [25:09] you don't need to put constantly you can [25:11] just put the value that you want to [25:12] update it to and that works well for a [25:15] single value like that but it also works [25:17] really well you have a collection of [25:19] values so how might that look like so [25:23] let's say we want to set Bao let's say [25:26] we wanted to change say let's say we [25:35] wanted to change the first point in the [25:40] first group in cat 1 so let's navigate [25:42] to this point whoops not navigate to the [25:45] repple let's navigate to this point and [25:47] change it so how would that look like [25:51] update value yeah here let's copy this [25:56] because it's gonna be similar to get [25:58] points ok that gets us to point but we [26:05] don't want to go to all and here's where [26:08] here's where I can show you a few other [26:10] features a few of the navigators so we [26:14] want to go to the first yeah my name rez [26:16] so the first group so that will navigate [26:19] us to here and then within catalyst we [26:22] also want to go to the first so we don't [26:24] wanna go to all of them we just want to [26:26] go to the first and then in points we [26:31] also want to replace the first point [26:36] so let's do first there as well okay so [26:42] that's on navigation that's navigators [26:43] to the first point and let's set that [26:45] one to be so instead of point one [26:48] start/stop let's just cool it something [26:52] completely different [26:52] let's call it point X let's do that from [26:57] data [26:57] I see if this works so rez we've [27:02] navigated to the first one which is [27:04] Group one and there and then within [27:06] catalyst we've navigated to the first [27:08] group Group one no here catalysts we've [27:12] navigated to the first one and here [27:16] you'll see we have two points and the [27:19] first point now has been replaced with [27:22] just the map point so we navigated all [27:24] the way to where we wanted and replaced [27:26] it with a specific value and because we [27:28] navigated to an element we even though [27:31] we replaced it with a data structure it [27:35] just replaced that element so that's [27:37] cool what about if we want to insert a [27:42] new value somewhere there's actually a [27:45] way to do that as well because Specter [27:49] has an ability not just to navigate to a [27:51] specific element or a specific sequence [27:53] within a date structure but can actually [27:55] navigate to a specific position and once [27:58] you're at that position then if you do [28:00] an insert then you can place things in [28:02] between before or after which is really [28:05] powerful so let's think what should we [28:07] wear should we insert something let's [28:09] insert a new point say let's insert [28:12] point three so after point two so let's [28:14] navigate basically to the same place [28:19] points but instead of navigating to the [28:22] first point yeah so instead of no this [28:25] is what this path will navigate is to [28:27] what we actually want to do is I [28:29] navigate to the last element now let me [28:33] remember how you do that L begin before [28:44] yeah before L or after um that was it [28:47] wasn't it before after [28:49] so this means after a particular element [28:52] so we navigate to the same place two [28:57] points within the first and the first so [29:00] we get to here and then we say after [29:03] element so it's going to navigate us to [29:04] this point in the data structure so then [29:06] if we set a value as H so that's add [29:13] point X again let's add point said us in [29:17] the data is the third argument and run [29:20] this hopefully it will have added just [29:23] the right point a new point called Z so [29:28] let's have a look so now we have three [29:31] points point one point two and point [29:33] said so didn't replace any of the [29:36] existing values but it navigated us to [29:38] the point in yeah in the data structure [29:43] to the position is maybe a better word [29:45] which you can then use to insert [29:47] elements without affecting what's [29:49] already there which is very powerful so [29:51] you've seen after element you saw that [29:53] there's before which would of course [29:54] let's change this to before just to show [29:56] you that would insert it at the [29:59] beginning of those oh I did I put it so [30:02] far nested down it takes us ages to find [30:04] it each time sorry about that yeah that [30:08] is expected now point Z is at the [30:09] beginning of the sequence of this vector [30:12] of maps not at the end and there's even [30:15] a macro function for going to a specific [30:18] place I think it's something like is it [30:23] enth [30:26] navigate to the specified indices let's [30:32] try this one so let's try end off one [30:43] points [30:45] No so that actually no end ha yeah so [30:49] this is the difference so aunt passed is [30:51] how you would navigate to a specific [30:54] element so what I've actually done is [30:57] navigated to the first element in the [30:59] list and then I've done set value so [31:01] that this is the equivalent of [31:03] place so this would be the equivalent of [31:05] doing last the enth path me just means [31:08] that because of course first and last [31:09] and he gets you the first and last [31:10] elements but if you have ten and you [31:12] want to navigate to seven then you could [31:14] do enter path six because it's zero [31:16] indexed so and thought isn't actually [31:19] the one that I was thinking of what was [31:20] the one that I was thinking of now is a [31:23] good time to introduce you to the [31:25] documentation that Specter has which is [31:27] actually I've gotten better a lot better [31:30] over time so on the github page there's [31:33] some really good examples that Nathan is [31:36] created to show you some of the [31:37] excellent things Specter can do so there [31:39] were well worth reading he's done some [31:41] fantastic presentations where he [31:43] explains it very clearly also worth [31:44] watching and then there are two wiki [31:46] pages one for navigators and one for the [31:48] macros and those two pages look like [31:51] this and what we're looking for so I've [31:54] shown you after element and before [31:56] element and I've shown you enth path [31:59] which is this one and navigate to the [32:01] specified indices which navigates you to [32:05] that element here before index this is [32:09] what we're looking for so we actually [32:10] want to navigate to the empty space [32:12] between the index and the prior index so [32:14] let's go back and let's do before index [32:18] one so that's gonna navigate me between [32:20] zero and one I think that's what we want [32:23] so let's try running this so hopefully [32:25] now this will insert it in between the [32:26] two points that we already have which it [32:31] does perfect so now you've seen the [32:34] difference between navigating to an [32:37] element and navigating to a position or [32:40] an index a very powerful feature okay [32:44] let's show you a couple more things [32:46] before we wrap up there's there's a lot [32:49] more to spec too than I'm showing you [32:50] right now but a really good ways to look [32:53] through these examples look through all [32:55] of and all of these have examples built [32:57] in so you can start to see all the [32:58] powerful things you can do I just wanted [33:00] to give you guys a taster but one of the [33:03] the features that I really like is it [33:05] has built in Walker so let's say we [33:08] wanted to use select so we're just going [33:10] to do a query and [33:12] we're gonna just use the walk and say [33:16] walk over the whole data structure but [33:19] only navigate to the numbers and let's [33:24] just see what happens if we do this yeah [33:27] exactly what we expect is navigated over [33:29] the entire data structure but only [33:32] stopped or only finished navigating when [33:35] it's got two values that are numbers so [33:38] that's a really quick and easy way to [33:40] yeah to get that out so Walker is a [33:44] fantastic way to navigate to all of the [33:48] nested leaves of any deeply nested data [33:53] structure which tends to look like a [33:54] tree and just make sure that you get all [33:57] of them out and then you can apply a [33:58] predicate to make sure you only see the [34:00] ones that you care about so let's just [34:03] give you another quick example if we've [34:04] changed this to string and we get all of [34:06] the strings out of the same data [34:07] structure really powerful [34:09] okay what about passing what happens if [34:12] we wanted to manipulate let's say these [34:15] times these are strings so as part of [34:19] our navigation we're going to be want to [34:21] be able to pass the string split it up [34:24] and then continue our navigation to a [34:27] part of that string so that we can then [34:29] do some manipulation so let's say what [34:31] did I write here round down all the stop [34:34] times to the nearest hour so let's say [34:36] we have a start time is 10:35 we want to [34:38] make that 10 o'clock so how would that [34:41] look like [34:42] so the way this works in Specter is that [34:45] it wants you to create a pass and an [34:48] unpassed function and the reason that it [34:51] needs both is because the way it works [34:53] you know how I explained that first it [34:55] navigates through your path all the way [34:57] down and then it reconstructs it back so [35:00] it as you navigate down and you do a [35:02] pass it will call your pass function to [35:03] split things up however you want and [35:05] then you can continue manipulating it [35:08] but then when it's reconstructing and [35:09] doing the reverse it needs an unpassed [35:11] function to put them back together so [35:13] how might that look like here so let's [35:15] say we will we gonna be passing a time [35:19] and what do we want to do there so let's [35:22] just do something like [35:24] close your string slash lip so we want [35:30] to split them with split it takes the [35:32] string first and then the regular [35:34] expression so we want to split time [35:36] based on the dot so and of course dot is [35:42] a reserved character and regular [35:44] expression so we're going to need to [35:46] escape it so we'll use backslash so [35:49] split the time I think that's right [35:52] that's what we want so if we pass 1035 [36:02] hopefully we get back 10 and 35 in a [36:05] vector that does exactly what once [36:06] that's all pass function now we just [36:08] need to do the opposite of that we need [36:10] to give them given two separate parts of [36:15] the time so giving us little time we [36:19] want to join them back together so we [36:21] can use the opposite function which [36:23] would be closure doc string slash join [36:29] think join works the other way yeah [36:31] first it wants the separator and then [36:33] the collection so we want to join it [36:35] together with the dot and we want to [36:38] join the split time [36:39] so the time would be a vector of strings [36:42] let's evaluate that and see if that [36:44] works so on pass given a vector of M and [36:54] two strings of 10 and 35 hopefully [36:57] they'll join them back together with the [36:58] dot in the middle yes so those two are [37:00] the perfect opposite functions so how [37:03] would we use this in inspector when we [37:06] navigating so if we what we actually [37:09] want to do is replace these values with [37:12] the rounded down value so we're going to [37:13] use this convenience function set valve [37:16] then we're gonna create a navigator and [37:18] we're gonna navigate [37:21] let's grab this one so I don't need to [37:23] type them while I begin all the way down [37:25] to the points and all we'll navigate [37:28] this to all of the maps and then we need [37:31] to go one step further because we [37:32] actually want to navigate to the stop [37:34] time and at this point we've now [37:37] gated - so let's just see what this [37:39] looks like if I turn this into set [37:42] select you can always use select as [37:45] you're building up your transforms or [37:47] your cells to see where you've got to so [37:49] what we've got - here are these stop [37:51] time strings which is exactly what we [37:53] want so if we go back to set valve now [37:56] what we can do is part of the navigation [37:57] we can pull one of the built-in [38:01] functions that Specter has called parser [38:05] which expects a pass function and the [38:07] non pass function so we just pass in [38:09] pass and on pass and now we'll navigate [38:14] this - and I shouldn't have changed this [38:17] to set well because we want to see what [38:18] this looks like so you can understand [38:20] what's going on here so now we its [38:22] navigate to this all the way two vectors [38:26] of strings where things have been split [38:28] so this is really nice because now we [38:30] can carry out work because we're within [38:32] the of course you could do the passing [38:35] as part of the function that you're [38:37] party of transform function you pass in [38:40] the string and you do this [38:42] transformation is this passing the [38:44] splitting but here we can carry on using [38:46] the power of the navigator so now we can [38:49] say okay all of those I want to just [38:51] navigate to the last one and in each of [38:54] these vectors so if we run this again [38:56] now we've just navigated to all of the [39:00] last parts of the stop times which is [39:02] exactly where we want it to get to and [39:04] now we want to set those if we go back [39:07] and change this to set Bal we would want [39:09] to set all of those to zero zero so we [39:11] want to round them down go to my [39:14] notifications and let's run this again [39:15] so now we should get back the whole date [39:18] structure let's navigate all the way [39:20] back down again you're getting used to [39:21] this data structure by now and let's [39:24] have a look [39:25] points and we have two and we have stop [39:30] time and they've both been rounded down [39:32] so before those were 1335 and 235 and [39:37] now we've successfully rounded them down [39:39] to one and two just what we wanted it's [39:44] a bit of a contrived example you might [39:45] not do it in this way but it's just to [39:47] show you how you can use other functions [39:50] with [39:51] your navigator to change how you're [39:53] seeing the data and then you can [39:55] manipulate it and because of how Spectre [39:57] magically puts it all back together for [39:59] you [39:59] you get very powerful results with very [40:02] little code so as I hope you can tell [40:06] I'm very excited about Spectre I use it [40:08] all the time in all the projects that I [40:10] write in closure and hopefully with this [40:13] quick overview about why it's [40:14] interesting a little bit about how it [40:16] works and then some examples of using it [40:18] I've increased your excitement level and [40:20] you're going to want to go and check [40:22] this out for yourself so I hope you do [40:25] thank you very much for watching and [40:26] I'll catch you next time thanks everyone [40:28] for by