Van Laarhoven lens
A van Laarhoven lens is a lens represented as a single function that is polymorphic over functors: given a way to turn the focus into an , it turns the whole structure into an . Choosing the functor chooses the operation: the constant functor gives the getter and the identity functor the setter[1]. Lenses in this form compose with ordinary function composition, and weakening the constraint from functors to applicative functors gives traversals, which compose with lenses in the same way[2]. The representation is named after Twan van Laarhoven, who described it in 2009, and it is the basis of Haskell's lens library[3].
Definition
A lens from a structure to a part is usually given as a getter and a setter, and . The van Laarhoven form is
A lens built from a getter and a setter applies the given function to the focus and maps the setter over the result:
The operations are recovered by choosing . With the constant functor , whose ignores its function, the focus passes through untouched and the rebuilding is discarded, which is . With the identity functor, whose is application, the result is the updated structure:
The two representations are equivalent: every function of the polymorphic type arises from a getter and a setter. Jaskelioff and O'Connor proved this as an instance of a general representation theorem, a consequence of the Yoneda lemma[7].
Type-changing lenses
The general form lets the update change the type of the focus, and with it the type of the structure:
so that a lens onto the first component of a pair can turn an (int, string) pair into a (float, string) pair. The simple is .
In OCaml
The definition quantifies over a type constructor , which OCaml's type system cannot express directly. Yallop and White's lightweight higher-kinded polymorphism represents the application of a type constructor to a type as a type ('a, 'f) app, where 'f is a brand, an ordinary type that stands for the constructor[4]. Functors become modules over a brand, and a lens is a record with a field polymorphic in the brand:
Van Laarhoven lenses in OCaml with brands, and the same lens used with four functors.
(* Higher-kinded types in OCaml: ('a, 'f) app stands for "'f applied to'a", where 'f is a brand, an abstract type naming a type constructor. *)type ('a, 'f) app = ..module type FUNCTOR = sigtype fval map : ('a -> 'b) -> ('a, f) app -> ('b, f) append(* A van Laarhoven lens: for every functor, lift a function on the focusto a function on the whole. *)type ('s, 'a) lens = { run : 'f. (module FUNCTOR with type f = 'f) -> ('a -> ('a, 'f) app) -> 's -> ('s, 'f) app }(* The identity functor gives over and set. *)type identitytype ('a, 'f) app += Identity : 'a -> ('a, identity) appmodule Identity = structtype f = identitylet map f = function Identity x -> Identity (f x) | _ -> assert falseend(* The constant functor gives view: the function's result is carriedthrough unchanged and the rebuilding is never done. *)(* The brand must be injective in 'r, so it is a variant, never built. *)type 'r const = Const_brand of 'rtype ('a, 'f) app += Const : 'r -> ('a, 'r const) appmodule Const (R : sig type t end) = structtype f = R.t constlet map _ = function Const r -> Const r | _ -> assert falseendlet view (type s a) (l : (s, a) lens) (s : s) : a =let module C = Const (struct type t = a end) inmatch l.run (module C) (fun a -> Const a) s with Const r -> r | _ -> assert falselet over (type s a) (l : (s, a) lens) (f : a -> a) (s : s) : s =match l.run (module Identity) (fun a -> Identity (f a)) s with Identity s -> s | _ -> assert falselet set l a = over l (fun _ -> a)(* A lens from a getter and a setter. *)let lens get put = { run = (fun (type f) (module F : FUNCTOR with type f = f) k s -> F.map (fun a -> put s a) (k (get s))) }(* Composition is function composition. *)let ( >> ) l1 l2 = { run = (fun m k -> l1.run m (l2.run m k)) }type address = { street : string; city : string }type person = { name : string; age : int; address : address }let address = lens (fun p -> p.address) (fun p a -> { p with address = a })let city = lens (fun a -> a.city) (fun a c -> { a with city = c })let age = lens (fun p -> p.age) (fun p a -> { p with age = a })(* Any other functor works with the same lens. With option: an updatethat may fail. With lists: every alternative. *)type ('a, 'f) app += Opt : 'a option -> ('a, [ `opt ]) appmodule Opt = struct type f = [ `opt ] let map f = function Opt o -> Opt (Option.map f o) | _ -> assert false endtype ('a, 'f) app += Lst : 'a list -> ('a, [ `lst ]) appmodule Lst = struct type f = [ `lst ] let map f = function Lst l -> Lst (List.map f l) | _ -> assert false end
Running it.
view (address >> city) Londonset (address >> city) "Oxford" { Ada, 36, Oxford }over age succ { Ada, 37, London }option functor, limit 40 { Ada, 37, London }option functor, limit 30 Nonelist functor { Ada, 36, London } { Ada, 36, Paris } { Ada, 36, Rome }original unchanged { Ada, 36, London }
The brands are constructors of an extensible type, so each functor adds its own constructor and matches on it. The same lens, unchanged, runs with the option functor, giving an update that can fail and leaves nothing changed if it does, and with the list functor, giving one updated record for each alternative. A lens written as a getter and setter pair would need a separate function for each of these.
Composition
Lenses in this form are functions that transform functions, so two of them compose with function composition:
A function on the innermost focus is lifted by to a function on the middle structure, and by to a function on the whole. The order is the order in which the fields are written in a path, outermost first, which is why Haskell code reads address . city as in an object-oriented language. With getter and setter pairs, composition has to be written out and pays for a nested get on every set.
Laws
A lawful lens satisfies get-put, put-get and put-put[5][8]:
In the van Laarhoven form the same conditions become laws of the kind satisfied by traversals: using the identity functor changes nothing, and running with a composed functor equals running twice. Composition of lawful lenses is lawful.
Traversals and the optic hierarchy
Replacing by gives a traversal, which can focus on any number of parts, since an applicative can combine the effects of many calls to the focus function[2]:
Every applicative is a functor, so every lens is a traversal, and a lens composed with a traversal is a traversal. In Haskell this subsumption is automatic, because constraints are checked at use; in OCaml an explicit coercion packs an applicative module as a functor module:
Traversals, lenses as traversals, and a composed traversal over the salaries of a company.
(* A traversal asks for an applicative instead of a functor, so it canvisit any number of foci and combine their effects. *)module type APPLICATIVE = siginclude FUNCTORval pure : 'a -> ('a, f) appval map2 : ('a -> 'b -> 'c) -> ('a, f) app -> ('b, f) app -> ('c, f) appendtype ('s, 'a) traversal = { trav : 'f. (module APPLICATIVE with type f = 'f) -> ('a -> ('a, 'f) app) -> 's -> ('s, 'f) app }(* Every lens is a traversal: an applicative is in particular a functor. *)let of_lens l = { trav = (fun (type f) (module A : APPLICATIVE with type f = f) k s -> l.run (module A : FUNCTOR with type f = f) k s) }let ( >>> ) t1 t2 = { trav = (fun m k -> t1.trav m (t2.trav m k)) }(* All elements of a list. *)let each = { trav = (fun (type f) (module A : APPLICATIVE with type f = f) k xs ->List.fold_right (fun x acc -> A.map2 List.cons (k x) acc) xs (A.pure [])) }module IdA = struct include Identity let pure x = Identity xlet map2 f a b = match (a, b) with Identity a, Identity b -> Identity (f a b) | _ -> assert false end(* Const into the monoid of lists collects the foci. *)module ConstList (R : sig type t end) = struct include Const (struct type t = R.t list end)let pure _ = Const [] let map2 _ a b = match (a, b) with Const a, Const b -> Const (a @ b) | _ -> assert false endlet to_list_of (type s a) (t : (s, a) traversal) (s : s) : a list =let module C = ConstList (struct type t = a end) inmatch t.trav (module C) (fun a -> Const [ a ]) s with Const r -> r | _ -> assert falselet over_all (type s a) (t : (s, a) traversal) (f : a -> a) (s : s) : s =match t.trav (module IdA) (fun a -> Identity (f a)) s with Identity s -> s | _ -> assert falsetype employee = { ename : string; salary : int }type company = { cname : string; staff : employee list }let staff = lens (fun c -> c.staff) (fun c s -> { c with staff = s })let salary = lens (fun e -> e.salary) (fun e s -> { e with salary = s })let salaries = of_lens staff >>> each >>> of_lens salary
Running it.
to_list_of salaries [100; 120; 90]over_all salaries (+10%) [110; 132; 99]names via each Ada, Charles, Mary
Viewing through a traversal uses the constant functor into a monoid, here lists, which is an applicative and collects all the foci. The standard traverse of a container is itself a traversal. Other constraints give the rest of the hierarchy: a getter asks for functors that are also contravariant, and folds for applicatives that are contravariant, so that no update is possible. Prisms and isomorphisms do not fit the functor form and need profunctors instead[6].
Profunctor optics
The profunctor representation generalizes the van Laarhoven one, quantifying over profunctors with some structure instead of functors[6]:
where the constraint determines the kind of optic: strong profunctors give lenses, choice profunctors prisms, and both together affine traversals. A van Laarhoven lens is a profunctor lens at the profunctors for functors . Riley described optics in general as a category built from a monoidal action, of which all of these are instances[9].
History
Lenses as getter and setter pairs came from work on bidirectional transformations and the view-update problem[5]. Van Laarhoven described the functor-polymorphic form in a blog post in 2009[1], and O'Connor observed in 2011 that the applicative version gives traversals and that lenses and traversals compose[2]. Kmett's lens library, released in 2012, built a hierarchy of optics on the representation[3]. Jaskelioff and O'Connor proved the representation theorem in 2015[7], and Pickering, Gibbons and Wu presented the profunctor generalization in 2017[6].
see also
- LensA first-class pair of a getter and a setter for one part of a structure, with laws that make them agree: you get back what you set, setting what you got changes nothing, and a second set overwrites the first. Lenses compose, so a path through nested records is one value.
- PrismThe counterpart of a lens for sum types: a partial getter that succeeds only on one constructor, and a builder that makes a whole value from that constructor's contents. A lens composed with a prism has at most one focus and no builder, which is called an affine traversal.
- FunctorIn functional programming, a functor is a type constructor f with an operation fmap : (a -> b) -> f a -> f b that applies a function inside the structure without changing its shape, preserving identity and composition. Lists, options, trees and functions out of a fixed type are functors. The notion comes from category theory. In OCaml the word also names parametrised modules such as Map.Make.
- ApplicativeAn applicative functor is a functor with pure : a -> f a and an operation that combines independent computations, <*> : f (a -> b) -> f a -> f b, or equivalently a product f a -> f b -> f (a * b), satisfying four laws. It sits between functors and monads: every monad is applicative, but because later steps cannot depend on earlier results, an applicative computation can collect all errors, run its parts in parallel, or be inspected before it runs.
- TraversableTraversable is the abstraction of a container that can be rebuilt while an applicative effect is performed on each element in order: traverse : (a -> f b) -> t a -> f (t b). Validating every element, parsing each string, numbering the nodes of a tree and taking cartesian products are all traversals with different applicative functors. traverse subsumes both map (with the identity functor) and foldMap (with a constant functor into a monoid), and it satisfies laws saying it visits every element exactly once.
- Yoneda lemmaFor a functor F and an object a, natural transformations from Hom(a, -) to F correspond one to one with elements of F a. In Haskell, forall b. (a -> b) -> f b is isomorphic to f a, and the left-hand form turns a chain of fmaps into one composed function and a single fmap.
referenced by
further reading
- [1]T. van Laarhoven, “CPS based functional references”, blog post (2009).
- [2]R. O’Connor, “Functor is to lens as applicative is to biplate: introducing multiplate”, Workshop on Generic Programming (2011).
- [3]E. Kmett, lens: lenses, folds and traversals, Haskell library (2012).
- [4]J. Yallop, L. White, “Lightweight higher-kinded polymorphism”, Functional and Logic Programming, LNCS 8475 (2014).
- [5]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).
- [6]M. Pickering, J. Gibbons, N. Wu, “Profunctor optics: modular data accessors”, The Art, Science, and Engineering of Programming 1 (2017).
- [7]M. Jaskelioff, R. O’Connor, “A representation theorem for second-order functionals”, Journal of Functional Programming 25 (2015).
- [8]S. Fischer, Z. Hu, H. Pacheco, “A clear picture of lens laws”, Mathematics of Program Construction, LNCS 9129 (2015).
- [9]M. Riley, “Categories of optics”, arXiv:1809.00738 (2018).
last updated