[com.sagevisuals/fn-in "5"]
com.sagevisuals/fn-in {:mvn/version "5"}
(require '[fn-in.core :refer [get-in* assoc-in* update-in* dissoc-in*]])
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.
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)
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.
(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
(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"}}]}]]}
(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]}]}
(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]}]}
clojure.core
Unless you absolutely need fn-in
's capabilities, use the built-ins.
A simple and declarative way to specify a structured computation, which is easy to analyze, change, compose, and monitor.
Path thread macros for navigating into and transforming data.
Querying and transforming nested and recursive data with a navigator abstraction.
A thing contained within a collection, either a scalar value or another nested collection.
Exactly one Clojure collection (vector, map, list, sequence, or set) with zero or more elements, nested to any depth.
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.)
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.
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
Ctrl+k | Jump to recent docs |
← | Move to previous article |
→ | Move to next article |
Ctrl+/ | Jump to the search field |