-- | -- -- Module: Optics -- Description: The main module, usually you only need to import this one. -- -- This library makes it possible to define and use 'Lens'es, 'Traversal's, -- 'Prism's and other /optics/, using an /abstract interface/. -- module Optics ( -- * Introduction -- $introduction -- ** What are optics? -- $what -- ** What is the abstract interface? -- $abstract -- ** Comparison with @lens@ -- $lens_comparison -- ** Other resources -- $otherresources -- * Using the library -- $basicusage module Optics.Optic -- ** Optic kinds #optickinds# , module O -- ** Optic operators , module Optics.Operators -- * Optics utilities -- ** At -- | An 'AffineTraversal' to traverse a key in a map or an element of a -- sequence: -- -- >>> preview (ix 1) ['a','b','c'] -- Just 'b' -- -- a 'Lens' to get, set or delete a key in a map: -- -- >>> set (at 0) (Just 'b') (Map.fromList [(0, 'a')]) -- fromList [(0,'b')] -- -- and a 'Lens' to insert or remove an element of a set: -- -- >>> IntSet.fromList [1,2,3,4] & contains 3 .~ False -- fromList [1,2,4] -- , module Optics.At -- ** Cons -- | 'Prism's to match on the left or right side of a list, vector or other -- sequential structure: -- -- >>> preview _Cons "abc" -- Just ('a',"bc") -- -- >>> preview _Snoc "abc" -- Just ("ab",'c') -- , module Optics.Cons -- ** Each -- | An 'IxTraversal' for each element of a (potentially monomorphic) container. -- -- >>> over each (*10) (1,2,3) -- (10,20,30) -- , module Optics.Each -- ** Empty -- | A 'Prism' for a container type that may be empty. -- -- >>> isn't _Empty [1,2,3] -- True -- , module Optics.Empty -- ** Re -- | Some optics can be reversed with 're'. This is mainly useful to invert -- 'Iso's: -- -- >>> let _Identity = iso runIdentity Identity -- >>> view (_1 % re _Identity) ('x', "yz") -- Identity 'x' -- -- Yet we can use a 'Lens' as a 'Review' too: -- -- >>> review (re _1) ('x', "yz") -- 'x' -- -- In the following diagram, red arrows illustrate how 're' transforms optics. -- The 'ReversedLens' and 'ReversedPrism' optic kinds are backwards versions -- of 'Lens' and 'Prism' respectively, and are present so that @'re' . 're'@ -- does not change the optic kind. -- -- <<diagrams/reoptics.png Reversed Optics>> -- , module Optics.Re -- ** ReadOnly -- | Defines 'getting', which turns a read-write optic into its read-only -- counterpart. , module Optics.ReadOnly -- ** 'Setter' utilities for working in 'Control.Monad.State.MonadState'. , module Optics.State -- ** View -- | A generalized view function 'gview', which returns a single result (like -- 'view') if the optic is a 'Getter', a 'Maybe' result (like 'preview') if -- the optic is an 'AffineFold', or a monoidal summary of results (like -- 'foldOf') if the optic is a 'Fold'. In addition, it works for any -- 'Control.Monad.Reader.MonadReader', not just @(->)@. -- -- >>> gview _1 ('x','y') -- 'x' -- -- >>> gview _Left (Left 'x') -- Just 'x' -- -- >>> gview folded ["a", "b"] -- "ab" -- -- >>> runReaderT (gview _1) ('x','y') :: IO Char -- 'x' -- -- This module is experimental. Using the more type-restricted variants is -- encouraged where possible. -- , module Optics.View -- ** Zoom -- | A class to 'zoom' in, changing the 'Control.Monad.State.State' supplied -- by many different monad transformers, potentially quite deep in a monad -- transformer stack. -- -- >>> flip execState ('a','b') $ zoom _1 $ equality .= 'c' -- ('c','b') -- , module Optics.Zoom -- * Indexed optics -- $indexed , module Optics.Indexed -- * Generation of optics -- ** ...with Template Haskell , module Optics.TH -- ** ...with @OverloadedLabels@ , module Optics.Label -- * Optics for concrete base types , module P ) where -- Core optics functionality -- for some reason haddock reverses the list... import Optics.Optic import Optics.Traversal as O import Optics.Setter as O import Optics.Review as O import Optics.ReversedPrism as O import Optics.Prism as O import Optics.ReversedLens as O import Optics.Lens as O import Optics.IxTraversal as O import Optics.IxSetter as O import Optics.IxFold as O import Optics.IxAffineTraversal as O import Optics.IxAffineFold as O import Optics.IxGetter as O import Optics.IxLens as O import Optics.Iso as O import Optics.Getter as O import Optics.Fold as O import Optics.AffineTraversal as O import Optics.AffineFold as O -- Optics utilities import Optics.At import Optics.Cons import Optics.Each import Optics.Empty import Optics.Indexed import Optics.Operators import Optics.Re import Optics.ReadOnly import Optics.State import Optics.View import Optics.Zoom -- Overloaded labels support import Optics.Label -- Template Haskell support import Optics.TH -- Optics for concrete base types import Data.Tuple.Optics as P import Data.Maybe.Optics as P import Data.Either.Optics as P -- $introduction -- -- Read on for a general introduction to the notion of optics, or if you are -- familiar with them already, you may wish to jump ahead to the "What is the -- abstract interface?" section below in "Optics#abstract". -- $what -- -- An optic is a first-class, composable /notion of substructure/. As a highly -- abstract concept, the idea can be approached by considering several examples -- of optics and understanding their common features. What are the possible -- relationships between some "outer" type @S@ and some "inner" type @A@? -- -- (For simplicity we will initially ignore the possibility of type-changing -- update operations, which change @A@ to some other type @B@ and hence change -- @S@ to some other type @T@. These are fully supported by the library, at the -- cost of some extra type parameters.) -- -- === "Optics.Iso": isomorphisms -- -- First, @S@ and @A@ may be /isomorphic/, i.e. there exist mutually inverse -- functions to convert @S -> A@ and @A -> S@. This is a somewhat trivial -- notion of substructure: @A@ is just another way to represent "all of @S@". -- -- An @'Iso'' S A@ is an isomorphism between @S@ and @A@, with the conversion -- functions given by 'view' and 'review'. For example, given -- -- @ -- newtype Age = Age Int -- @ -- -- there is an isomorphism between the newtype and its representation: -- -- @ -- 'coerced' :: 'Iso'' Age 'Int' -- 'view' 'coerced' :: Age -> 'Int' -- 'review' 'coerced' :: 'Int' -> Age -- @ -- -- === "Optics.Lens": generalised fields -- -- If @S@ is a simple product type (i.e. it has a single constructor with one or -- more fields), @A@ may be a single field of @S@. More generally, @A@ may be -- "part of @S@" in the sense that @S@ is isomorphic to the pair @(A,C)@ for -- some type @C@ representing the other fields. In this case, there is a -- /projection/ function @S -> A@ for getting the value of the field, but the -- /update/ function (setting the value of the field) requires the "rest of @S@" -- and so has type @A -> S -> S@. -- -- A @'Lens'' S A@ captures the structure of @A@ being a field of @S@, with the -- projection function given by 'view' and the update function by 'set'. For -- example, for the pair type @(X,Y)@ there are lenses for each component: -- -- @ -- '_1' :: 'Lens'' (X,Y) X -- '_2' :: 'Lens'' (X,Y) Y -- 'view' '_1' :: (X,Y) -> X -- 'set' '_2' :: Y -> (X,Y) -> (X,Y) -- @ -- -- (Note that the update function could arguably have the more precise type @A -- -> C -> S@, since we do not expect the result of setting a field to depend on -- the previous value of the field. However, making @C@ explicit turns out to -- be awkward, so instead we impose /laws/ to require that the result of setting -- the field depends only on @C@, and, more generally, that the lens behaves as -- we would expect.) -- -- === "Optics.Prism": generalised constructors -- -- If @S@ is a simple sum type (i.e. it has one or more constructors, each with -- a single field), @A@ may be the type of the field for a single constructor of -- @S@. More generally, @S@ may be isomorphic to the disjoint union @Either D -- A@ for some type @D@ representing the other constructors. In this case, -- projecting out @A@ from @S@ (pattern-matching on the constructor) may fail, -- so it has type @S -> Maybe A@. In the reverse direction we have a function -- of type @A -> S@ representing the constructor itself. -- -- A @'Prism'' S A@ captures the structure of @A@ being a constructor of @S@, -- with the partial projection function given by 'preview' and the constructor -- function given by 'review'. For example, for the type @'Either' X Y@ there -- is a prism for each constructor: -- -- @ -- '_Left' :: 'Prism'' ('Either' X Y) X -- '_Right' :: 'Prism'' ('Either' X Y) Y -- 'preview' '_Left' :: 'Either' X Y -> 'Maybe' X -- 'review' '_Right' :: Y -> 'Either' X Y -- @ -- -- === "Optics.Traversal": multiple substructures -- -- Alternatively, @S@ may "contain" the substructure @A@ a variable number of -- times. In this case, the projection function extracts the (possibly zero or -- many) elements so has type @S -> [A]@, while the update function may take -- different values for different elements so has type @(A -> A) -> S -> S@ -- (though in fact more general formulations are possible). -- -- A @'Traversal'' S A@ captures the structure of @A@ being contained in @S@ -- perhaps multiple times, with the list of values given by 'toListOf' and the -- update function given by 'over' . For example, for the type @Maybe X@ there -- is a traversal that may return zero or one element: -- -- @ -- 'traversed' :: 'Traversal'' ('Maybe' X) X -- 'toListOf' 'traversed' :: 'Maybe' X -> [X] -- 'over' 'traversed' :: (X -> X) -> 'Maybe' X -> 'Maybe' X -- @ -- -- (In fact, traversals of at most one element are known as /affine/ traversals, -- see "Optics.AffineTraversal".) -- -- -- === In general -- -- So far we have seen four different kinds of optic or "notions of -- substructure", and many more are possible. Observe the important properties -- they have in common: -- -- * There are subtyping relationships between different optic kinds. Any -- isomorphism is trivially a lens and a prism (with no other fields or -- constructors, respectively). Any lens is a traversal (where the list of -- elements is always a singleton list), and any prism is also a traversal -- (where there will be zero or one element depending on whether the -- constructor matches). This was implicit in the fact that we used that we -- used the same operators in multiple cases: 'view' gives the projection -- function of both an isomorphism and a lens, but cannot be applied to a -- traversal. -- -- * Optics can be composed. If @S@ is isomorphic to @U@ and @U@ is isomorphic -- to @A@ then @S@ is isomorphic to @A@, and similarly for other optic kinds. -- -- * Composition and subtyping interact: a lens and a prism can be composed, by -- first thinking of them as traverals using the subtyping relationship. That -- is, if @S@ has a field @U@, and @U@ has a constructor @A@, then @S@ -- contains zero or one @A@s that we can pick out with a traversal (but in -- general there is neither a lens from @S@ to @A@ nor a prism). -- -- * Each optic kind can be described by certain operations it enables. For -- example lenses support projection and update, while prisms support partial -- projection and construction. -- -- * Optics are subject to laws, which are necessary for the operations to make -- sense. -- -- The point of the @optics@ library is to capture this common pattern. -- $abstract #abstract# -- -- A key principle behind this library is the belief that optics are useful as -- an abstract concept, and that the purpose of types is to capture abstract -- concepts and make them useful. The programmer using optics should be able to -- think in terms of the abstract interface, rather than the details of the -- implementation, and implementation choices should (as far as possible) not -- dictate the interface. -- -- Each optic kind is identified by a "tag type" (such as 'A_Lens'), which is an -- empty data type. The type of the actual optics (such as 'Lens') is obtained -- by applying the 'Optic' newtype wrapper to the tag type. -- -- @ -- type 'Lens' s t a b = 'Optic' 'A_Lens' 'NoIx' s t a b -- type 'Lens'' s a = 'Optic'' 'A_Lens' 'NoIx' s a -- @ -- -- 'NoIx' as the second parameter to 'Optic' indicates that the optic is not -- indexed. See the "Indexed optics" section below in "Optics#indexed" for -- further discussion of indexed optics. -- -- The details of the internal implementation of 'Optic' are hidden behind an -- abstraction boundary, so that the library can be used without needing to -- think about the particular implementation choices. -- -- -- === Specification of optics interfaces -- -- Each different kind of optic is documented in a separate module describing -- its abstract interface, in a standard format with at least /formation/, -- /introduction/, /elimination/, and /well-formedness/ sections. See "Optic -- kinds" below in "Optics#optickinds" for a list of these modules. -- -- * The __formation__ sections contain type definitions. For example -- "Optics.Lens" defines: -- -- @ -- -- Type synonym for a type-modifying lens. -- type 'Lens' s t a b = 'Optic' 'A_Lens' 'NoIx' s t a b -- @ -- -- * The __introduction__ sections describe the canonical way to construct each -- particular optic. Continuing with a 'Lens' example: -- -- @ -- -- Build a lens from a getter and a setter. -- 'lens' :: (s -> a) -> (s -> b -> t) :: 'Lens' s t a b -- @ -- -- * Correspondingly, the __elimination__ sections show how you can destruct the -- optic into the pieces from which it was constructed. -- -- @ -- -- A 'Lens' is a 'Setter' and a 'Getter', therefore you can specialise types to obtain -- 'view' :: 'Lens' s t a b -> s -> a -- 'set' :: 'Lens' s t a b -> b -> s -> t -- @ -- -- * The __computation__ rules tie introduction and elimination forms -- together. These rules are automatically fulfilled by the library (for -- well-formed optics). -- -- @ -- 'view' ('lens' f g) s ≡ f s -- 'set' ('lens' f g) a s ≡ g s a -- @ -- -- * The __well-formedness__ sections describe the laws that each optic should -- obey. As far as possible, all optics provided by the library are -- well-formed, but in some cases this depends on invariants that cannot be -- expressed in types. Ill-formed optics /might/ behave differently from what -- the computation rules specify. -- -- For example, a 'Lens' should obey three laws, known as /GetPut/, /PutGet/ -- and /PutPut/. See the "Optics.Lens" module for their definitions. The -- user of the 'lens' introduction form must ensure that these laws are -- satisfied. -- -- * Some optic kinds have __additional introduction forms__, -- __additional elimination forms__ or __combinators__ sections, which give -- alternative ways to create and use optics of that kind. In principle these -- are expressible in terms of the canonical introduction and elimination -- rules. -- -- * The __subtyping__ section gives the "tag type" (such as 'A_Lens'), which in -- particular is accompanied by 'Is' instances that define the subtyping -- relationship discussed in the following section. -- -- -- === Subtyping -- -- There is a subtyping relationship between optics, implemented using -- typeclasses. The 'Is' typeclass captures the property that one optic kind -- can be used as another, and the 'castOptic' function can be used to -- explicitly cast between optic kinds. 'Is' forms a partial order, represented -- in the graph below. For example, a lens can be used as a traversal, so there -- are arrows from 'Lens' to 'Traversal' (via 'AffineTraversal') and there is an -- instance of @'Is' 'A_Lens' 'A_Traversal'@. -- -- Introduction forms (constructors) return a concrete optic kind, while -- elimination forms (destructors) are generally polymorphic in the optic kind -- they accept. This means that it is not normally necessary to explicitly cast -- between optic kinds. For example, we have -- -- @ -- 'view' :: 'Is' k 'A_Getter' => 'Optic'' k is s a -> s -> a -- @ -- -- so 'view' can be used with isomorphisms or lenses, as these can be converted -- to a 'Getter'. -- -- If an explicit cast is needed, you can use 'castOptic'. This arises when you -- use optics of different kinds in a context that requires them to have the -- same type. For example @['folded', 'traversed']@ gives a type error (since -- 'A_Traversal' is not 'A_Fold') but @['folded', 'castOptic' 'traversed']@ -- works. -- -- The optic kind module (e.g. "Optics.Lens") does not list all ways to -- construct or use particular the optic kind. For example, since a 'Lens' is -- also a 'Traversal', a 'Fold' etc, so you can use 'traverseOf', 'preview' and -- many other combinators with lenses. -- -- -- ==== Subtype hierarchy -- -- This graph gives an overview of the optic kinds and their subtype -- relationships: -- -- <<diagrams/optics.png Optics hierarchy>> -- -- In addition to the optic kinds included in the diagram, there are also -- indexed variants such as 'IxLens', 'IxGetter', 'IxAffineTraversal', -- 'IxTraversal', 'IxAffineFold', 'IxFold' and 'IxSetter'. These are explained -- in more detail in the "Indexed optics" section below in "Optics#indexed". -- -- -- === Composition -- -- Since /optics are not functions/, they cannot be composed with the ('.') -- operator. Instead there is a separate composition operator ('%'). The -- composition operator returns the common supertype of its arguments, or -- generates a type error if the composition does not make sense. -- -- The optic kind resulting from a composition is the least upper bound (join) -- of the optic kinds being composed, if it exists. The 'Join' type family -- computes the least upper bound given two optic kind tags. For example the -- 'Join' of a 'Lens' and a 'Prism' is an 'AffineTraversal'. -- -- >>> :kind! Join A_Lens A_Prism -- Join A_Lens A_Prism :: * -- = An_AffineTraversal -- -- The join does not exist for some pairs of optic kinds, which means that they -- cannot be composed. For example there is no optic kind above both 'Setter' -- and 'Fold': -- -- >>> :kind! Join A_Setter A_Fold -- Join A_Setter A_Fold :: * -- = (TypeError ...) -- -- >>> :t mapped % folded -- ... -- ...A_Setter cannot be composed with A_Fold -- ... -- $lens_comparison -- -- The @lens@ package is the best known Haskell library for optics, and -- established many of the foundations on which the @optics@ package builds (not -- least in quite a bit of code having been directly ported). It defines optics -- based on the /van Laarhoven/ representation, where each optic kind is -- introduced as a /transparent/ type synonym for a complex polymorphic type, -- for example: -- -- @ -- type Lens s t a b = forall f. 'Functor' f => (a -> f b) -> s -> f t -- @ -- -- In contrast, @optics@ tries to preserve an abstraction boundary between the -- interface of optics and their implementation. Optic kinds are expressed -- directly in the types, as 'Optic' is an /opaque/ newtype: -- -- @ -- type 'Lens' s t a b = 'Optic' 'A_Lens' 'NoIx' s t a b -- @ -- -- The choice of representation of 'Optic' is then an implementation detail, not -- essential for understanding the library. (In fact, @optics@ uses the -- /profunctor/ representation rather than the /van Laarhoven/ representation; -- this affects the optic kinds and operations that can be conveniently -- supported, but not the essence of the design.) -- -- Our design choice to use /opaque/ rather than /transparent/ abstractions -- leads to various consequences, both positive and negative, which are explored -- in the following subsections. -- -- == Advantages of the opaque design -- -- Since the interface is deliberately chosen rather than to some extent -- determined by the implementation, we are free to choose a more restricted -- interface where doing so leads to conceptual simplicity. For example, in -- @lens@, the 'view' function can be used with a 'Fold' provided the result -- type has a 'Monoid' instance, and the multiple targets of the 'Fold' will be -- combined monoidally. This behaviour can be confusing, so in @optics@ a -- 'Fold' cannot be silently used as a 'Getter', and we prefer to have 'view' -- work on 'Getter's and define a separate 'foldOf' operator for use on -- 'Fold's. (But the 'gview' function is available for users who may prefer -- otherwise.) -- -- In general, opaque abstractions lead to better results from type inference -- (the optic kind is preserved in the inferred type): -- -- >>> :t traversed % to not -- traversed % to not -- :: Traversable t => Optic A_Fold '[] (t Bool) (t Bool) Bool Bool -- -- Error messages are domain-specific: -- -- >>> set (to fst) -- ... -- ...A_Getter cannot be used as A_Setter -- ... -- -- Composing incompatible optics yields a sensible error: -- -- >>> sets map % to not -- ... -- ...A_Setter cannot be composed with A_Getter -- ... -- -- Since 'Optic' is a rank-1 type, it is easy to store optics in a -- datastructure: -- -- >>> :t [folded, backwards_ folded] -- [folded, backwards_ folded] :: Foldable f => [Fold (f a) a] -- -- It is possible to define aliases for optics without the monomorphism -- restriction spoiling the fun: -- -- >>> let { myoptic = _1; p = ('x','y') } in (view myoptic p, set myoptic 'c' p) -- ('x',('c','y')) -- -- Finally, having an abstract interface gives more freedom of choice in the -- internal implementation. If there is a compelling reason to switch to an -- alternative representation, one can in principle do so without changing the -- interface. -- -- -- == Disadvantages of the opaque design -- -- Since 'Optic' is a newtype, other libraries that wish to define optics must -- depend upon its definition. In contrast, with a transparent representation, -- and since the van Laarhoven representations of lenses and traversals depend -- only on definitions from @base@, it is possible for libraries to define them -- without any extra library dependencies (although this does not hold for more -- advanced optic kinds such as prisms or indexed optics). To address this, the -- present library is split into a package @optics-core@, which has a minimal -- dependency footprint intended for use in libraries, and the -- \"batteries-included\" @optics@ package for use in applications. -- -- It is something of an amazing fact that the composition operator for -- transparent optics is just function composition. Moreover, since Haskell -- uses ('.') for function composition, @lens@ is able to support a pseudo-OOP -- syntax. In contrast, @optics@ must use a different composition operator -- ('%'). 'Optic' does not quite form a 'Control.Category.Category', thanks to -- type-changing optics. -- -- Rather than emerging naturally from the definitions, opportunities for -- polymorphism have to be identified in advance and explicitly introduced using -- type classes. Similarly, the set of optic kinds and the subtyping -- relationships between them must be fixed in advance, and cannot be added to -- in downstream libraries. Thus in a sense the opaque approach is more -- restrictive than the transparent one. There are cases in @lens@ where the -- types work out nicely and permit abstraction-breaking-but-convenient -- shortcuts, such as applying a 'Traversal' as a 'traverse'-like function, -- whereas @optics@ requires a call to 'traverseOf'. -- -- -- == More specific differences -- -- The sections above set out the major conceptual differences from the @lens@ -- package, and their advantages and disadvantages. Some more specific design -- differences, which may be useful for comparison or porting code between the -- libraries. This list is no doubt incomplete. -- -- * The composition operator is ('%') rather than ('.') and is defined as -- @infixl 9@ instead of @infixr 9@. -- -- * Fewer operators are provided, and some of them are not exported from the -- main "Optics" module. Import "Optics.State.Operators" if you want them. -- -- * The 'view' function and corresponding ('Optics.Operators.^.') operator work -- only for 'Getter's and have a more restricted type. The equivalent for -- 'Fold's is 'foldOf', and you can use 'preview' for -- 'AffineFold's. Alternatively you can use 'gview' which is more compatible -- with @view@ from @lens@, but it uses a type class to choose between 'view', -- 'preview' and 'foldOf'. -- -- * Indexed optics are rather different, as described in the "Indexed optics" -- section below in "Optics#indexed". All ordinary optics are -- "index-preserving", so there is no separate notion of an index-preserving -- optic. -- -- * 'Each' provides indexed traversals. -- -- * @firstOf@ from @lens@ is replaced by 'headOf'. -- -- * @concatOf@ from @lens@ is omitted in favour of the more general 'foldOf'. -- -- * 'set'' is a strict version of 'set', not 'set' for type-preserving optics. -- -- * Numbered lenses for accessing fields of tuples positionally are provided -- only up to '_9', rather than @_19@. -- -- * There are four variants of @backwards@ for (indexed) 'Traversal's and -- 'Fold's: 'backwards', 'backwards_', 'ibackwards' and 'ibackwards_'. -- -- * There is no @Traversal1@ and @Fold1@. -- -- * There are affine variants of (indexed) traversals and folds -- ('AffineTraversal', 'AffineFold', 'IxAffineTraversal' and 'IxAffineFold'). -- An affine optic targets at most one value. Composing a 'Lens' with a -- 'Prism' produces an 'AffineTraversal', so for example @'matching' ('_1' '%' -- '_Left')@ is well-typed. -- -- * Functions 'ifiltered' and 'indices' are defined as optic combinators due to -- restrictions of internal representation. -- -- * We can't use 'traverse' as an optic directly. Instead there is a -- 'Traversal' called 'traversed'. Similarly 'traverseOf' must be used to -- apply a 'Traversal', rather than simply using it as a function. -- -- * The 're' combinator produces a different optic kind depending on the kind -- of the input 'Iso', for example 'Review' reverses to 'Getter' while a -- reversed 'Iso' is still an 'Iso'. Thus there is no separate @from@ -- combinator for reversing 'Iso's. -- $otherresources -- -- * <https://skillsmatter.com/skillscasts/10692-through-a-glass-abstractly-lenses-and-the-power-of-abstraction Through a Glass, Abstractly: Lenses and the Power of Abstraction> a talk on the principles behind this library with <https://github.com/well-typed/optics/raw/master/Talk.pdf accompanying slides> by Adam Gundry (but note that the design details of @optics@ have changed substantially since this talk was given) -- -- * <https://skillsmatter.com/skillscasts/12360-profunctors-and-data-accessors Profunctors and Data Accessors> a talk on basics of profunctors and how they relate to data accessors such as lenses, prisms and traversals by Andrzej Rybczak -- -- * <https://www.cs.ox.ac.uk/people/jeremy.gibbons/publications/poptics.pdf Profunctor Optics: Modular Data Accessors> a paper by Matthew Pickering, Jeremy Gibbons and Nicolas Wu -- -- * <https://oleg.fi/gists/posts/2017-04-18-glassery.html Glassery> and <https://oleg.fi/gists/posts/2017-04-26-indexed-poptics.html Indexed Profunctor optics>, blog posts by Oleg Grenrus -- -- * The <https://hackage.haskell.org/package/lens lens> package by Edward Kmett and contributors -- $basicusage -- -- To get started, you can just add @optics@ as a dependency to your @.cabal@ -- file, and then: -- -- @ -- import "Optics" -- @ -- -- If you are writing a library for which it is important to keep the dependency -- footprint minimal, you may wish to depend upon @optics-core@ instead (and -- perhaps @optics-extra@ or @optics-th@), and then: -- -- @ -- import "Optics.Core" -- @ -- $indexed #indexed# -- -- The @optics@ library also provides indexed optics, which provide -- an additional /index/ value in mappings: -- -- @ -- 'over' :: 'Setter' s t a b -> (a -> b) -> s -> t -- 'iover' :: 'IxSetter' i s t a b -> (i -> a -> b) -> s -> t -- @ -- -- Note that there aren't any laws about indices. -- Especially in compositions the same index may occur multiple times. -- -- The machinery builds on indexed variants of 'Functor', 'Foldable', and 'Traversable' classes: -- 'FunctorWithIndex', 'FoldableWithIndex' and 'TraversableWithIndex' respectively. -- There are instances for types in the boot libraries. -- -- @ -- class ('FoldableWithIndex' i t, 'Traversable' t) -- => 'TraversableWithIndex' i t | t -> i where -- 'itraverse' :: 'Applicative' f => (i -> a -> f b) -> t a -> f (t b) -- @ -- -- Indexed optics /can/ be used as regular ones, i.e. indexed optics -- gracefully downgrade to regular ones. -- -- >>> toListOf ifolded "foo" -- "foo" -- -- >>> itoListOf ifolded "foo" -- [(0,'f'),(1,'o'),(2,'o')] -- -- But there is also a combinator 'noIx' to explicitly erase indices: -- -- >>> :t (ifolded % simple) -- (ifolded % simple) -- :: FoldableWithIndex i f => Optic A_Fold '[i] (f b) (f b) b b -- -- >>> :t noIx (ifolded % simple) -- noIx (ifolded % simple) -- :: FoldableWithIndex i f => Optic A_Fold NoIx (f b) (f b) b b -- -- @ -- λ> :t noIx (ifolded % ifolded) -- noIx (ifolded % ifolded) -- :: (FoldableWithIndex i1 f1, FoldableWithIndex i2 f2) => -- Optic A_Fold NoIx (f1 (f2 b)) (f1 (f2 b)) b b -- @ -- -- As the example above illustrates, regular and indexed optics have the same -- tag in the first parameter of 'Optic', in this case 'A_Fold'. Regular optics -- simply don't have any indices. The provided type aliases 'IxLens', -- 'IxGetter', 'IxAffineTraversal', 'IxAffineFold', 'IxTraversal', 'IxFold' and -- 'IxSetter' are variants with a single index. In general, the second parameter -- of the 'Optic' newtype is a type-level list of indices, which will typically -- be 'NoIx' (the empty index list) or @('WithIx' i)@ (a singleton list). -- -- When two optics are composed with ('%'), the index lists are concatenated. -- Thus composing an unindexed optic with an indexed optic preserves the -- indices, or composing two indexed optics retains both indices: -- -- @ -- λ> :t (ifolded % ifolded) -- (ifolded % ifolded) -- :: (FoldableWithIndex i1 f1, FoldableWithIndex i2 f2) => -- Optic A_Fold '[i1, i2] (f1 (f2 b)) (f1 (f2 b)) b b -- @ -- -- In order to use such an optic, it is necessary to flatten the indices into a -- single index using 'icompose' or a similar function: -- -- @ -- λ> :t icompose (,) (ifolded % ifolded) -- icompose (,) (ifolded % ifolded) -- :: (FoldableWithIndex i1 f1, FoldableWithIndex i2 f2) => -- Optic A_Fold (WithIx (i1, i2)) (f1 (f2 b)) (f1 (f2 b)) b b -- @ -- -- For example: -- -- >>> itoListOf (icompose (,) (ifolded % ifolded)) [['a','b'], ['c', 'd']] -- [((0,0),'a'),((0,1),'b'),((1,0),'c'),((1,1),'d')] -- -- Alternatively, you can use one of the ('<%') or ('%>') operators to compose -- indexed optics and pick the index to retain, or the ('<%>') operator to -- retain a pair of indices: -- -- >>> itoListOf (ifolded <% ifolded) [['a','b'], ['c', 'd']] -- [(0,'a'),(0,'b'),(1,'c'),(1,'d')] -- -- >>> itoListOf (ifolded %> ifolded) [['a','b'], ['c', 'd']] -- [(0,'a'),(1,'b'),(0,'c'),(1,'d')] -- -- >>> itoListOf (ifolded <%> ifolded) [['a','b'], ['c', 'd']] -- [((0,0),'a'),((0,1),'b'),((1,0),'c'),((1,1),'d')] -- -- In the diagram below, the optics hierachy is amended with these (singly) indexed variants (in blue). -- Orange arrows mean -- "can be used as one, assuming it's composed with any optic below the -- orange arrow first". For example. '_1' is not an indexed fold, but -- @'itraversed' % '_1'@ is, because it's an indexed traversal, so it's -- also an indexed fold. -- -- >>> let fst' = _1 :: Lens (a, c) (b, c) a b -- >>> :t fst' % itraversed -- fst' % itraversed -- :: TraversableWithIndex i f => -- Optic A_Traversal '[i] (f a, c) (f b, c) a b -- -- <<diagrams/indexedoptics.png Indexed Optics>> -- $setup -- >>> import Control.Monad.Reader -- >>> import Control.Monad.State -- >>> import Data.Functor.Identity -- >>> import qualified Data.IntSet as IntSet -- >>> import qualified Data.Map as Map -- >>> import Optics.State.Operators