[funcool/lentes "1.3.3"]
Lentes is an implementation of functional references modeled as functions (in the same way as transducers). Lenses are a generalization of get and put mapping to a particular part of a data structure.
Please see the Quick Start for an overview of what lentes has to offer.
Since lentes is a young project there can be some API breakage.
The simplest way to use lentes in a clojure project, is by including it in the dependency vector on your project.clj file:
[funcool/lentes "1.3.3"]
Do not be scared with the package name and its description. It is pretty straightforward to use and understand. A lens consists out of couple of functions responsible for reading and altering nested data structures.
Let’s create a getter and setter function:
(defn my-getter
[state]
(:foo state))
(defn my-setter
[state f]
(update state :foo f))
These can be used to access or modify data:
(def data {:foo 1 :bar 2})
(my-getter data)
;; => 1
(my-setter data inc)
;; => {:foo 2 :bar 2}
We can generalize the getting and setting using lenses abstraction:
(require '[lentes.core :as l])
(def mylens (l/lens my-getter my-setter))
(l/focus mylens state)
;; => 1
(l/over mylens inc state)
;; => {:foo 2 :bar 2}
Lentes comes with some helpers for creating commonly used lenses. As a
result we don’t have to write our own my-getter
and my-setter
functions for common use-cases. Keep reading to find out what helpers
are provided with lentes.
(def mylens (l/key :foo))
(l/focus mylens state)
;; => 1
(l/over mylens inc state)
;; => {:foo 2 :bar 2}
Lentes succinctly interfaces with atoms. The l/derive
function
allows you to create derived atoms that act like limited versions of
the original atom. This technique is also named materialized views
.
(def state1 (atom {:foo 1 :bar 1}))
(def state2 (l/derive (l/key :foo) state1))
(satisfies? IAtom state2)
;; => true
@state2
;; => 1
(swap! state2 inc)
@state2
;; => 2
@state1
;; => {:foo 2 :bar 2}
The derived atoms has very useful properties such as they are lazy (no code executed until is really needed) and smart (does not trigger watches if the focused data is not affected).
This is especially useful, when you want to create materialized views of the global state, and use them together with rum facilities to react on atom changes, allowing components to re-render only when the affected state is changed.
Lentes offers functions to easily define composable lenses.
Just as important are a handful of functions that take a lens and data. When called they return either the value or modified data.
The examples below cover basic usage of lentes' lenses. It also
demonstrates what the focus
, put
and over
functions do.
The most basic lens is the identity
lens. It allows you to get and
set the data as is.
The focus
function allows you to get the value the lens is "focused"
on.
In this example we create an identity lens. We then call the focus function with the lens and a vector as arguments. It doesn’t get any simpler then this.
(require '[lentes.core :as l])
(l/focus l/id [0 1 2 3])
;; => [0 1 2 3]
As you can see focus
just returned the data as is.
We have two other core functions:
put
allows us to set a value a lens is focusing on.
over
lets us apply a function over the focused value of a lens.
(l/put l/id 42 [0 1 2 3])
;; => 42
(l/over l/id count [0 1 2 3])
;; => 4
We have only mentioned the id
lens. Lentes provides more lens helpers. It’s
also possible to create your own lenses for your specific needs.
There are some builtin lenses that work on sequences. These are the fst
,
snd
and nth
lens:
fst
lens;; Focus over the first element of a vector
(l/focus l/fst [1 2 3])
;; => 1
;; Apply a function over first element of a vector
(l/over l/fst inc [1 2 3])
;; => [2 2 3]
;; Replace the first value of an element of a vector
(l/put l/fst 42 [1 2 3])
;; => [42 2 3]
nth
lens(l/focus (l/nth 2) [1 2 3])
;; => 3
(l/over (l/nth 2) inc [1 2 3])
;; => [1 2 4]
(l/put (l/nth 2) 42 [1 2 3])
;; => [1 2 42]
There’s key
and select-keys
for focusing on one or multiple keys respectively:
(l/focus (l/key :a) {:a 1 :b 2})
;; => 1
(l/over (l/key :a) str {:a 1 :b 2})
;; => {:a "1", :b 2}
(l/put (l/key :a) 42 {:a 1 :b 2})
;; => {:a 42, :b 2}
(l/focus (l/select-keys [:a]) {:a 1 :b 2})
;; => {:a 1}
(l/over (l/select-keys [:a :c])
(fn [m]
(zipmap (keys m) (repeat 42)))
{:a 1 :b 2})
;; => {:b 2, :a 42}
(l/put (l/select-keys [:a :c])
{:a 0}
{:a 1 :b 2 :c 42})
;; => {:b 2, :a 0}
in
for focusing on a path:
(l/focus (l/in [:a :b])
{:a {:b {:c 42}}})
;; => {:c 42}
(l/over (l/in [:a :b]) #(zipmap (vals %) (keys %))
{:a {:b {:c 42}}})
;; => {:a {:b {42 :c}}}
(l/put (l/in [:a :b])
42
{:a {:b {:c 42}}})
;; => {:a {:b 42}}
Let’s take a look at a combinator that will let us build a unit-conversion lens
called units
. We have to supply a function to convert from unit a
to unit b
and viceversa:
(defn sec->min [sec] (/ sec 60))
(defn min->sec [min] (* min 60))
(def mins (l/units sec->min
min->sec))
(l/focus mins 120)
;; => 2
(l/put mins 3 120)
;; => 180
(l/over mins inc 60)
;; => 120
Conditional lenses are defined using a predicate function as argument. It only focuses on the value when the called predicate returns true. The predicate is called with the value as argument.
(l/focus (l/passes even?) 2)
;; => 2
(l/over (l/passes even?) inc 2)
;; => 3
(l/put (l/passes even?) 42 2)
;; => 42
(l/focus (l/passes even?) 1)
;; => nil
(l/over (l/passes even?) inc 1)
;; => 1
(l/put (l/passes even?) 42 1)
;; => 1
One of the big advantages of this lenses implementation is because it is implemented in terms of function composition, much in the same line as transducers. Let see a example:
(def my-lens (comp l/fst l/fst (l/nth 2)))
(def data
[[0 1 2]
[3 4 5]])
(l/focus my-lens data)
;; => 2
(l/put my-lens 42 data)
;; => [[0 1 42] [3 4 5]]
Lenses compose with regular function composition and, like transducers, the combined lens runs from left to right.
Five most important rules:
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Readability counts.
All contributions to lentes should keep these important rules in mind.
Please read CONTRIBUTING.md
file on the root of repository.
lentes is open source and can be found on github.
You can clone the public repository with this command:
git clone https://github.com/funcool/lentes
For running tests just execute this:
clojure -Adev tools build:tests
node ./target/tests.js
clojure -Adev -m lentes.tests
lentes is licensed under BSD (2-Clause) license:
Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz> All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Can you improve this documentation? These fine people already did:
Andrey Antukh & bas080Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close