wiki

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].

§ 01

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 Prism
type shape = Circle of float | Rect of float * float
let 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 2
None
Circle 3
Circle 4
Rect (1, 2)

The last line is the useful property in practice: a modification through a prism that does not match leaves the value alone.

§ 02

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].

§ 03

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.

Isos ≅ aLensexactly one focusPrismzero or one; can buildAffine traversalzero or oneGetterread oneReviewbuild onlyTraversalany numberFoldread manySettermodify manyan arrow means “can be used as”
The optics hierarchy. Each optic can be used as any optic below it; composing two optics gives the most specific optic below both. A lens composed with a prism is an affine traversal.

A path into a JSON document built from prisms and key lookups, modified where it exists.

open Prism
type json = Null | Num of float | Str of string | Arr of json list | Obj of (string * json) list
let 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 may
be 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].

§ 04

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].

§ 05

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

referenced by

further reading

  1. [1]M. Pickering, J. Gibbons, N. Wu, “Profunctor optics: modular data accessors”, The Art, Science, and Engineering of Programming 1 (2017).
  2. [2]E. Kmett, the lens library, Hackage (2012).
  3. [3]M. Riley, “Categories of optics”, arXiv:1809.00738 (2018).
  4. [4]T. Rendel, K. Ostermann, “Invertible syntax descriptions: unifying parsing and pretty printing”, Haskell Symposium (2010).
  5. [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. [6]G. Boisseau, J. Gibbons, “What you needa know about Yoneda: profunctor optics and the Yoneda lemma”, ICFP (2018).

last updated