Liking cljdoc? Tell your friends :D

Setup
API
Changelog
Introduction
Ideas
Performance
Examples
Alternatives
Glossary
Contact

fn-in

A library for manipulating heterogeneous, arbitrarily-nested Clojure data structures

Setup

Leiningen/Boot

[com.sagevisuals/fn-in "5"]

Clojure CLI/deps.edn

com.sagevisuals/fn-in {:mvn/version "5"}

Require

(require '[fn-in.core :refer [get-in* assoc-in* update-in* dissoc-in*]])

Introduction

Most of the time, when someone hands us a Clojure collection, we know its  type and how best to manipulate it. If we get handed a vector, we know that get is effective at retrieving an element. Let's grab that 3.

(get [0 1 2 3 4 5] 2) ;; => 2

But sometimes, we won't know ahead of time what kind of collection  someone might give us. Perhaps we made a commitment that our utility would  handle all Clojure collection types. Instead of a vector, we might be given a lazy  sequence, such as a range. Perhaps we want to pull out the third element.

(get (range 6) 2) ;; => nil

That's… not what we wanted. While get, assoc, update, and dissoc and their ...-in cousins are exceedingly handy, they do not seamlessly handle every collection  type.

We could certainly build up an ad hoc type dispatch right on the spot inside our utility, but we'll probably  forget to include at least one of the collection types, and we're likely to  miss an edge case or two.

This library endeavors to patch those gaps in functionality while  maintaining a consistent, familiar interface to inspect, change, and remove elements contained in vectors, hashmaps, sequences, lists, and sets, at any arbitrary  level of nesting.

Leaning on those extended capabilities, we can now retrieve that tenth  element with a straightforward, drop-in replacement.

(require '[fn-in.core :refer [get*]])

(get* (range 6) 2) ;; => 2

get* behaves just like clojure.core/get, except that it succeeds in extracting the third element from our range argument.

Ideas

This library provides starred versions of the core utilities — get-in*, assoc-in*, update-in*, dissoc-in* — which all operate similar to their clojure.core namesakes, but work on any heterogeneous, arbitrarily-nested data structure.

Their interface is based on the concept of a path. A path unambiguously addresses an element of a heterogeneous,  arbitrarily-nested data structure. Elements in vectors, lists, and other  sequences are addressed by zero-indexed integers. Hashmap elements are  addressed by their keys, and set elements are addressed by the elements  themselves.

Here's how paths work. Vectors are addressed by zero-indexed integers.

           [100 101 102 103]
indexes --> 0 1 2 3

Same for lists…

          '(97 98 99 100)
indexes --> 0 1 2 3

…and same for other sequences, like range.

(range 29 33) ;; => (29 30 31 32)
indexes -----------> 0 1 2 3

Hashmaps are addressed by their keys, which are often keywords, like  this.

        {:a 1 :foo "bar" :hello 'world}
keys --> :a :foo :hello

But hashmaps may be keyed by any value, including integers…

        {0 "zero" 1 "one" 99 "ninety-nine"}
keys --> 0 1 99

…or some other scalars…

        {"a" :value-at-str-key-a 'b :value-at-sym-key-b \c :value-at-char-key-c}
keys --> "a" 'b \c

…even composite values.

        {[0] :val-at-vec-0 [1 2 3] :val-at-vec-1-2-3 {} :val-at-empty-map}
keys --> [0] [1 2 3] {}

Set elements are addressed by their identities, so they are located at  themselves.

             #{42 :foo true 22/7}
identities --> 42 :foo true 22/7

A path is a sequence of indexes, keys, or identities that  allow the starred functions to dive into a nested data structure, one path  element per level of nesting. Practically, any sequence may serve as a path,  but a vector is convenient to make with a keyboard.

Let's build a path to the third element of a vector [11 22 33]. Vector elements are addressed by zero-indexed integers, so the third  element is located at integer 2. We invoke get-in* just like clojure.core/get-in: the collection is the first arg. We stuff that 2 into the second arg, the path sequence.

(get-in* [11 22 33] [2]) ;; => 33

And we receive the third element, integer 33.

Let's get a little more fancy: a vector nested within another vector [11 22 [33 44 55]]. The nested vector is located at the third spot, index 2. If we call get-in* with that path…

(get-in* [11 22 [33 44 55]] [2]) ;; => [33 44 55]

…it dutifully tells us that there's a child vector nested at that spot. To  access the third element of that vector, we must append an entry onto the path.

(get-in* [11 22 [33 44 55]] [2 2]) ;; => 55

Nothing terribly special that clojure.core/get-in can't do. But, if for some reason, that nested thing is instead a list…

(get-in [11 22 '(33 44 55)] [2 2]) ;; => nil

…it's not quite what we wanted. But if we invoke the starred version…

(get-in* [11 22 '(33 44 55)] [2 2]) ;; => 55

…all fine and dandy.

Let's look at hashmaps. Hashmap elements are addressed by keys. Let's  inspect the value at key :z. We insert a :z keyword into the path arg.

(get-in* {:y 22, :z 33, :x 11} [:z]) ;; => 33

If there's another hashmap nested at that key, and we wanted the value at  keyword :w of that nested hashmap, we would merely append that key to the previous path  vector arg.

(get-in* {:y 22, :z {:q 44, :w 55}, :x 11} [:z :w]) ;; => 55

Again, that's exactly how clojure.core/get-in works, but what if we had something else nested there, like a clojure.lang.Range?

(get-in {:y 11, :z (range 30 33)} [:z 2]) ;; => nil

That nil may not be useful for what we need to do. But calling the starred version…

(get-in* {:y 11, :z (range 30 33)} [:z 2]) ;; => 32

…we get that 32 we wanted.

Beyond inspecting a value with get-in*, the starred functions can return a modified copy of a heterogeneous,  arbitrarily-nested data structure. They all consume a path exactly the way get-in* does. First, we could swap out — associating — a nested value for one we supply.

Let's try associating an element contained in a clojure.lang.Cycle nested in a list, nested in a hashmap. Going three collections 'deep'  requires a three-element path.

(assoc-in* {:a (list 11 (take 3 (cycle ['foo 'bar 'baz])))} [:a 1 2] :Clojure!)
;; => {:a (11 (foo bar :Clojure!))}

We could also apply a function to — updating — a nested value. Let's add 9977 to an integer contained in a vector, nested in a clojure.lang.Repeat. Diving two levels deep requires a two-element path.

(update-in* (take 3 (repeat [11 22 33])) [2 1] #(+ % 9977))
;; => ([11 22 33] [11 22 33] [11 9999 33])

Or, we can simply dissociate a nested value, removing it entirely. Let's dissociate an integer element  contained in a clojure.lang.Iterate, nested in a list.

(dissoc-in* {:a (list 22 (take 3 (iterate inc 33)))} [:a 1 1])
;; => {:a (22 (33 35))}

Note how the starred functions are able to dive into any of the collection  types to do their jobs. These capabilities allow us to straightforwardly  manipulate any Clojure data we might encounter.

(clojure.core does not provide an equivalent dissoc-in companion to dissoc.)

The empty vector addresses the top-level root collection of any  collection type.

(get-in* [1 2 3] []) ;; => [1 2 3]

(get-in* '(:foo "bar" 42) []) ;; => (:foo "bar" 42)

(get-in* {:a 1, :b 2} []) ;; => {:a 1, :b 2}

(get-in* #{:foo 42 \z} []) ;; => #{42 :foo \z}

(get-in* (range 0 4) []) ;; => (0 1 2 3)

Performance

The fn-in library endeavors to be a drop-in replacement for get-in, assoc-in, and friends, mimicking their signatures, semantics, etc. Where possible, fn-in delegates to those core functions, thus closely matching the performance expectations of the core functions, plus a smidgen of overhead for type  dispatch.

Some operations, such as manipulating the end of a list, will inevitably  be slow. But even if not performant, at least the operation is possible.

Examples

Getting values

(get-in* [11 22 [33 44 55 [66 [77 [88 99]]]]] [2 3 1 1 1]) ;; => 99

(get-in* {:a {:b {:c {:d 99}}}} [:a :b :c :d]) ;; => 99

(get-in* (list 11 22 33 (list 44 (list 55))) [3 1 0]) ;; => 55

(get-in* #{11 #{22}} [#{22} 22]) ;; => 22

(get-in* [11 22 {:a 33, :b [44 55 66 {:c [77 88 99]}]}] [2 :b 3 :c 2]) ;; => 99

(get-in* {:a (list {} {:b [11 #{33}]})} [:a 1 :b 1 33]) ;; => 33

Associating values

(assoc-in* [11 [22 [33 [44 55 66]]]] [1 1 1 2] :new-val)
;; => [11 [22 [33 [44 55 :new-val]]]]

(assoc-in* {:a {:b {:c 42}}} [:a :b :c] 99) ;; => {:a {:b {:c 99}}}

(assoc-in* {:a [11 22 33 [44 55 {:b [66 {:c {:d 77}}]}]]}   [:a 3 2 :b 1 :c :d]   "foo") ;; => {:a [11 22 33 ;; [44 55 {:b [66 {:c {:d "foo"}}]}]]}

Updating values

(update-in* [11 22 33 [44 [55 66 [77 88 99]]]] [3 1 2 2] inc)
;; => [11 22 33 [44 [55 66 [77 88 100]]]]

(update-in* {:a [11 22 {:b 33, :c [44 55 66 77]}]} [:a 2 :c 1] #(+ 5500 %)) ;; => {:a [11 22 {:b 33, :c [44 5555 66 77]}]}

Dissociating values

(dissoc-in* [11 22 [33 [44 55 66]]] [2 1 1]) ;; => [11 22 [33 [44 66]]]

(dissoc-in* {:a [11 22 33 {:b 44, :c [55 66 77]}]} [:a 3 :c 0]) ;; => {:a [11 22 33 {:b 44, :c [66 77]}]}

Alternatives

  • clojure.core

    Unless you absolutely need fn-in's capabilities, use the built-ins.

  • Plumatic's Plumbing

    A simple and declarative way to specify a structured computation, which is easy to analyze, change, compose, and monitor.

  • John Newman's Injest

    Path thread macros for navigating into and transforming data.

  • Nathan Marz' Specter

    Querying and transforming nested and recursive data with a navigator abstraction.

Glossary

element

A thing contained within a collection, either a scalar value or another nested collection.

heterogeneous, arbitrarily-nested data structure

Exactly one Clojure collection (vector, map, list, sequence, or set) with zero or more elements, nested to any depth.

non-terminating sequence

One of clojure.lang.{Cycle,Iterate,LazySeq,LongRange,Range,Repeat} that may or may not be realized, and possibly infinite. (I am not aware of any way to determine if such a sequence is infinite, so I treat them as if they are.)

path

A series of values that unambiguously navigates to a single element (scalar or sub-collection) in a heterogeneous, arbitrarily-nested data structure. In the context of the fn-in library, the series of values comprising a path is contained in a vector passed as the second argument to the namespace's …-in functions. Almost identical to the second argument of clojure.core/get-in, but with more generality.

Elements of vectors, lists, and other sequential collections are located by zero-indexed integers. Map values are addressed by their keys, which are often keywords, but can be any data type, including integers, or composite types. (You don't often need to key a map on a multi-element, nested structure, but when you need to, it's awesome.) Set members are addressed by their identities. Nested collections contained in a set can indeed be addressed: the path vector itself contains the collections. An empty vector [] addresses the outermost, containing collection.


License

This program and the accompanying materials are made available under the terms of the MIT License.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close