wiki

Lens

A record update in a language with immutable data is shallow. Changing the city inside the address inside a person means rebuilding every record on the way down by hand. A lens packages one step of the way down, as a getter and a setter, and composing two lenses gives the path through both[1][3]. Three laws make the pair behave like a field, even when the part is computed rather than stored. Lenses were introduced by Foster, Pierce and colleagues in 2005 as bidirectional transformations for the view-update problem of databases[1][4], and became a standard tool of functional programming through van Laarhoven's encoding of 2009[2] and Kmett's Haskell lens library[6]. A prism is the corresponding optic for sum types, and profunctor optics give a uniform account of the whole family[3].

§ 01

Example

A lens as a record of two functions, with composition, for a person with a nested address.

(* A lens focuses on one part 'a of a structure 's. *)
type ('s, 'a) lens = { get : 's -> 'a; set : 'a -> 's -> 's }
(* Composition: focus with l1, then with l2 inside that. *)
let ( |-- ) l1 l2 =
{ get = (fun s -> l2.get (l1.get s)); set = (fun a s -> l1.set (l2.set a (l1.get s)) s) }
let over l f s = l.set (f (l.get s)) s
type address = { city : string; street : string }
type person = { name : string; address : address; born : int }
let address = { get = (fun p -> p.address); set = (fun a p -> { p with address = a }) }
let city = { get = (fun a -> a.city); set = (fun c a -> { a with city = c }) }
let street = { get = (fun a -> a.street); set = (fun s a -> { a with street = s }) }
let born = { get = (fun p -> p.born); set = (fun b p -> { p with born = b }) }

Reading, setting and modifying through a composed lens.

open Lens

Running it.

London
St James's Sq, Paris
unchanged: St James's Sq, London
ST JAMES'S SQ

The original value is unchanged: set builds a new person with a new address, sharing the parts that did not change.

adaname = "Ada"address = •born = 1815its addresscity = "London"street = …movedname = "Ada"address = •born = 1815new addresscity = "Paris"street = …get"London""Paris"setstreet sharedname, born copied
Getting through address |-- city follows the path down. Setting rebuilds the records on the path from the bottom up, and shares or copies everything off the path.
§ 02

Laws

A lens from to is a pair and satisfying[1]:

The laws are what make a lens more than two functions that happen to share a type. They say that splits into the focus and a complement that leaves alone, so a lens can be used without reading its definition. They are preserved by composition. A lens that satisfies the first two is called well-behaved, and one that satisfies all three very well-behaved[1].

A lens need not correspond to a stored field. A virtual field computed from others is a lens if setting it adjusts the underlying data consistently, and a lens that changes anything besides its focus breaks set-get.

A computed lens, and one that does too much, checked against the laws on random values.

open Lens

Running it.

address |-- city get-set 0 set-get 0 set-set 0 failures in 1000
age get-set 0 set-get 0 set-set 0 failures in 1000
address |-- sloppy get-set 0 set-get 505 set-set 0 failures in 1000

The sloppy lens trims the street whenever it sets the city, so setting the city to its current value is not a no-op; the failure shows up only when a random street has surrounding spaces.

§ 03

Van Laarhoven lenses

Van Laarhoven observed that a lens can be encoded as a single function, polymorphic over a functor[2]:

The function takes an action on the focus and lifts it to the whole structure. Choosing the functor chooses the operation: the constant functor turns it into a getter, the identity functor into a modifier. Composition is ordinary function composition, and a lens library needs no lens type at all, only type synonyms; a lens written this way depends only on the base library. The van Laarhoven lens article treats the encoding in detail.

Van Laarhoven lenses in Haskell, without a library.

{-# LANGUAGE RankNTypes #-}
import Data.Functor.Const (Const (..))
import Data.Functor.Identity (Identity (..))
-- A van Laarhoven lens: one function, polymorphic in the functor.
type Lens s a = forall f. Functor f => (a -> f a) -> s -> f s
lens :: (s -> a) -> (a -> s -> s) -> Lens s a
lens getter setter k s = fmap (\a -> setter a s) (k (getter s))
-- Choosing the functor chooses the operation.
view :: Lens s a -> s -> a
view l s = getConst (l Const s)
over :: Lens s a -> (a -> a) -> s -> s
over l f s = runIdentity (l (Identity . f) s)
set :: Lens s a -> a -> s -> s
set l a = over l (const a)
data Address = Address { city :: String, street :: String } deriving Show
data Person = Person { name :: String, address :: Address } deriving Show
addressL :: Lens Person Address
addressL = lens address (\a p -> p { address = a })
cityL :: Lens Address String
cityL = lens city (\c a -> a { city = c })
-- Composition is ordinary function composition.
main :: IO ()
main = do
let ada = Person "Ada" (Address "London" "St James's Sq")
putStrLn (view (addressL . cityL) ada)
print (set (addressL . cityL) "Paris" ada)
-- Any functor works: a list of results gives every variant of an edit.
print (map (city . address) ((addressL . cityL) (\c -> [c, c ++ "!"]) ada))

Running it.

London
Person {name = "Ada", address = Address {city = "Paris", street = "St James's Sq"}}
["London","London!"]

The last line uses the list functor, which the getter–setter pair cannot express: it applies a nondeterministic edit to the focus and returns every resulting structure. Replacing Functor by Applicative gives traversals, which focus on any number of parts[7]. The encoding needs higher-kinded polymorphism, which OCaml has only through module functors, so OCaml libraries usually represent lenses as records of functions.

§ 04

Theory

A lens is equivalently a function , returning the focus together with a way to rebuild the whole from a new focus. This is a coalgebra of the store comonad , and the lens laws are exactly the laws of a comonad coalgebra[5]. Lenses are also the case of optics for the product: a lens is a way of writing for some hidden complement . Replacing the product by a sum gives prisms, and profunctor optics describe lenses, prisms, traversals and the rest uniformly, as functions polymorphic over profunctors with the appropriate structure[3][8].

§ 05

History

The view-update problem, of translating an update to a database view back to the underlying tables, was studied by Bancilhon and Spyratos in 1981[4]. Foster, Greenwald, Moore, Pierce and Schmitt introduced lenses as bidirectional tree transformations for the Harmony synchronizer in 2005, with the laws in their present form[1]. Functional references appeared in several Haskell libraries in the following years. Van Laarhoven described the functor encoding in 2009[2], O'Connor related it to traversals[7], and Kmett's lens library of 2012 built a hierarchy of optics on it[6]. Pickering, Gibbons and Wu gave the profunctor account in 2017[3], and Riley a categorical one in 2018[8].

see also

referenced by

further reading

  1. [1]J. N. Foster, M. B. Greenwald, J. T. Moore, B. C. Pierce, A. Schmitt, “Combinators for bidirectional tree transformations: a linguistic approach to the view-update problem”, ACM Transactions on Programming Languages and Systems 29 (2007).
  2. [2]T. van Laarhoven, “CPS based functional references”, blog post (2009).
  3. [3]M. Pickering, J. Gibbons, N. Wu, “Profunctor optics: modular data accessors”, The Art, Science, and Engineering of Programming 1 (2017).
  4. [4]F. Bancilhon, N. Spyratos, “Update semantics of relational views”, ACM Transactions on Database Systems 6 (1981).
  5. [5]J. Gibbons, M. Johnson, “Relating algebraic and coalgebraic descriptions of lenses”, Electronic Communications of the EASST 49 (2012).
  6. [6]E. Kmett, the lens library, Hackage (2012).
  7. [7]R. O’Connor, “Functor is to lens as applicative is to biplate: introducing multiplate”, arXiv:1103.2841 (2011).
  8. [8]M. Riley, “Categories of optics”, arXiv:1809.00738 (2018).

last updated