Prism
A lens always finds its focus, because every record has every field. A value of a sum type is one case out of several, so looking inside a particular case can fail, and a prism's getter returns an option. In exchange, a prism can go backwards: the contents of the case are enough to build the whole value[1][2]. Just as a lens splits a structure into a focus and a complement, , a prism splits it into a focus or something else, [1][3]. The name comes from Kmett's Haskell lens library of 2012, where prisms are one level of a hierarchy of optics[2]; profunctor optics describe them uniformly with the rest[1].
Example
A prism as a record, with modification, composition and affine traversals.
(* A prism focuses on one case of a sum type: preview matches, review builds. *)type ('s, 'a) prism = { preview : 's -> 'a option; review : 'a -> 's }(* Modify the focus if the case matches; leave the value alone otherwise. *)let over p f s = match p.preview s with Some a -> p.review (f a) | None -> s(* Composing two prisms gives a prism. *)let ( >>> ) p q ={ preview = (fun s -> Option.bind (p.preview s) q.preview); review = (fun a -> p.review (q.review a)) }(* An affine traversal: at most one focus, and no way to build from it.Composing a lens with a prism, or looking up a key, gives one. *)type ('s, 'a) affine = { find : 's -> 'a option; replace : 'a -> 's -> 's }let of_prism p = { find = p.preview; replace = (fun a s -> match p.preview s with Some _ -> p.review a | None -> s) }let ( |>> ) (x : ('s, 'a) affine) (y : ('a, 'b) affine) ={ find = (fun s -> Option.bind (x.find s) y.find);replace = (fun b s -> match x.find s with Some a -> x.replace (y.replace b a) s | None -> s) }let modify x f s = match x.find s with Some a -> x.replace (f a) s | None -> s
A prism onto the Circle case of a shape type.
open Prismtype shape = Circle of float | Rect of float * floatlet circle = { preview = (function Circle r -> Some r | _ -> None); review = (fun r -> Circle r) }let show = function| Circle r -> Printf.sprintf "Circle %g" r| Rect (w, h) -> Printf.sprintf "Rect (%g, %g)" w h
Running it.
Some 2NoneCircle 3Circle 4Rect (1, 2)
The last line is the useful property in practice: a modification through a prism that does not match leaves the value alone.
Laws
A prism from to is a pair and satisfying[1]:
The first says that building and then matching gives back what was put in, so is injective. The second says that a successful match loses nothing, so the matched values are exactly the image of . A prism between strings and integers, parsing one way and printing the other, is the standard example of what the second law excludes: several strings parse to the same integer, but only one of them is printed back.
Checking the laws for parsing and printing integers, and for a version that accepts only canonical strings.
open Prism
Running it.
int_string review-preview fails on 0, preview-review fails on: "007", "+5", "0x1F"canonical review-preview fails on 0, preview-review fails on: none
Parser–printer pairs that satisfy weaker laws, called partial isomorphisms, underlie invertible syntax descriptions, from which both a parser and a printer are derived[4].
Composition and the optics hierarchy
Two prisms compose to a prism: match the outer case, then the inner one, and build in the other order. A lens and a prism compose to something weaker than either: it may fail to find its focus, like a prism, but cannot build a whole value from the focus alone, like a lens. This is an affine traversal, also called an optional. Looking up a key in an association list or a JSON object is another example.
A path into a JSON document built from prisms and key lookups, modified where it exists.
open Prismtype json = Null | Num of float | Str of string | Arr of json list | Obj of (string * json) listlet num = { preview = (function Num n -> Some n | _ -> None); review = (fun n -> Num n) }let obj = { preview = (function Obj kv -> Some kv | _ -> None); review = (fun kv -> Obj kv) }(* The value at a key of an object: an affine traversal, since the key maybe missing. *)let key k =of_prism obj |>> {find = List.assoc_opt k;replace = (fun v kv -> List.map (fun (k', v') -> if k' = k then (k', v) else (k', v')) kv) }let rec show = function| Null -> "null"| Num n -> Printf.sprintf "%g" n| Str s -> Printf.sprintf "%S" s| Arr l -> "[" ^ String.concat ", " (List.map show l) ^ "]"| Obj kv -> "{" ^ String.concat ", " (List.map (fun (k, v) -> Printf.sprintf "%S: %s" k (show v)) kv) ^ "}"
Running it: the age is incremented, and the two paths that do not exist, or reach a string rather than a number, leave the document unchanged.
{"user": {"name": "Ada", "age": 37}, "tags": []}{"user": {"name": "Ada", "age": 36}, "tags": []}{"user": {"name": "Ada", "age": 36}, "tags": []}
Composition follows the hierarchy: the composite of two optics is the most specific kind that both can be used as. Isos, invertible functions, are both lenses and prisms; traversals focus on any number of parts; folds and setters are their read-only and write-only halves[2].
Encodings
In the van Laarhoven style used by the Haskell lens library, a lens is a function polymorphic over a functor, and a traversal one polymorphic over an applicative functor. A prism needs to build as well as match, which a function cannot express, so the library generalizes the arrow to a profunctor[1][2]:
Here is the class of profunctors that can pass through a sum, just as lenses need profunctors that pass through a product. Pickering, Gibbons and Wu showed that every optic in the hierarchy is a function of the form , polymorphic over profunctors with a particular structure, and that composition is again function composition[1]; Boisseau and Gibbons related this representation to the concrete one through the Yoneda lemma[6].
History
Bidirectional transformations, of which lenses are the best known, were studied as a solution to the view-update problem[5]. Prisms were introduced in Kmett's lens library for Haskell in 2012, together with the hierarchy of isos, traversals, folds and setters[2]. Pickering, Gibbons and Wu gave the profunctor account in 2017[1], and Riley's categorical treatment of optics in 2018 characterized prisms as the optics for coproducts[3].
see also
- LensA lens is a 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 immutable records is one value that can read or update the part at its end. They originated in work on the view-update problem, and in Haskell are usually encoded as functions polymorphic in a functor, which makes composition ordinary function composition.
- Van Laarhoven lensA van Laarhoven lens is a lens represented as a single function polymorphic over functors: forall f. Functor f => (a -> f a) -> s -> f s. Instantiating the functor gives the operations: the constant functor yields the getter and the identity functor the setter. Lenses in this form compose with ordinary function composition, and replacing Functor with Applicative gives traversals, which compose with lenses the same way. The representation is the basis of Haskell's lens library.
- Polymorphic variantA polymorphic variant is a variant whose constructors, called tags and written with a backquote in OCaml, exist independently of any type declaration. Their types are sets of tags with bounds: [> `A] means at least `A, [< `A | `B] means at most those two, and unification works out unions and intersections of those sets using row variables. They let functions share constructors across types, be extended with new cases without editing old code, and encode subtyping, at the cost of longer types and error messages and of typos in open types going unnoticed.
- 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
- lens in HaskellBuilding a lens from first principles, then the real library: Lens, Prism, Traversal, Iso, Fold, indexed optics, and generic derivation.
- Lenses and Prisms in OCamlAn optics library whose kinds are a phantom polymorphic variant, and a ppx that derives lenses from records, prisms from variants, and both from field paths and patterns.
further reading
- [1]M. Pickering, J. Gibbons, N. Wu, “Profunctor optics: modular data accessors”, The Art, Science, and Engineering of Programming 1 (2017).
- [2]E. Kmett, the lens library, Hackage (2012).
- [3]M. Riley, “Categories of optics”, arXiv:1809.00738 (2018).
- [4]T. Rendel, K. Ostermann, “Invertible syntax descriptions: unifying parsing and pretty printing”, Haskell Symposium (2010).
- [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]G. Boisseau, J. Gibbons, “What you needa know about Yoneda: profunctor optics and the Yoneda lemma”, ICFP (2018).
last updated