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:

Example using 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]
Example using the 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:

Example focusing in a specific key/keys of associative data structure
(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:

Example focusing in nested data structures
(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:

Example defining a "unit conversion" lens
(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.

Example focusing using conditional lenses
(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:

Run tests on node platform
./scripts/build
node ./out/tests.js
Run tests on JVM platform
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.