Introduction
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.
Note
|
This documentation does not cover all of the API. You can browse the full API documentation here. |
Project Maturity
Since lentes is a young project there can be some API breakage.
Install
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.2.0"]
Quick Start
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.
Introduction to lenses
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}
Working with atoms
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.
Reference
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.
Builtin lenses
Identity
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.
Sequences
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]
Associative data structures
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
Conditionals
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
Composition
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.
Developers Guide
Philosophy
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.
Contributing
Please read CONTRIBUTING.md
file on the root of repository.
Source Code
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
Run tests
For running tests just execute this:
./scripts/build
node ./out/tests.js
lein test
License
lentes is licensed under BSD (2-Clause) license:
Copyright (c) 2015-2016 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.