We are delighted to announce the first Hackage release of
optics, a Haskell library for
defining and using lenses, traversals, prisms and other optic kinds. The
optics library is broadly similar in functionality to the well-established
lens library, but uses an abstract interface rather than exposing the
underlying implementation of each optic kind. It aims to be easier to understand
than lens, with clearer interfaces, simpler types and better error messages,
while retaining as much functionality as possible. It is being developed by
Andrzej Rybczak and Adam Gundry, with significant contributions from Oleg
Grenrus and Andres Löh, and much copy-pasting of code and selective copying of
ideas from lens.
Example of optics types and error messages
Let’s dive straight into an example of using optics in GHCi. What is a lens?
*Optics> :info Lens
type Lens s t a b = Optic A_Lens NoIx s t a bThe Optic newtype unifies different optic kinds such as lenses, traversals
and prisms. Its first type parameter, here A_Lens, indicates the optic kind in
use. The second, NoIx, means that this is not an indexed optic (we will mostly
ignore indexed optics for the purposes of this post). As in lens, the s and
t parameters represent the types of the outer structure (before and after a
type-changing update), and the a and b parameters represent the types of the
inner field.
A lens can be constructed using, naturally enough, the lens function, which
takes getter and setter functions and returns a Lens (i.e. an Optic A_Lens):
*Optics> :type lens
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
*Optics> let l = lens (\(x,_) -> x) (\(_,y) x -> (x,y))
l :: Lens (a1, b) (a2, b) a1 a2Given a lens we can use it to view the inner value within the outer structure,
or set a new value:
*Optics> :type view
view :: Is k A_Getter => Optic' k is s a -> s -> a
*Optics> :type set
set :: Is k A_Setter => Optic k is s t a b -> b -> s -> tNotice that these types are polymorphic in the optic kind k they accept, but
specify very clearly what kind of optic they require.1 You
can apply view to any optic kind k that can be converted to (i.e. is a
subtype of) a Getter. The Is constraint implements subtyping using the
typeclass system. In particular, we have instances for Is A_Lens A_Getter and
Is A_Lens A_Setter so our lens l can be used with both operators:
*Optics> view l ('a','b')
'a'
*Optics> set l 'c' ('a','b')
('c','b')If you try to use an optic kind that is not a subtype of the required type, a clear error message is given:
*Optics> :type sets
sets :: ((a -> b) -> s -> t) -> Setter s t a b
*Optics> :type view (sets fmap)
<interactive>:1:1: error:
    • A_Setter cannot be used as A_Getter
    • In the expression: view (sets fmap)Composing optics
Optics are not functions, so they cannot be composed with the (.)
operator. This may be viewed as a price to pay for the improved type inference
and clearer type errors, but it is conceptually important: we regard optics as
an abstract concept distinct from possible representations using functions, so
it does not make sense to compose them with function composition or apply them
with function application.2
Instead of (.), a separate composition operator (%) is
provided:3
*Optics> :type l % l
l % l :: Optic A_Lens '[] ((a, b1), b2) ((b3, b1), b2) a b3
*Optics> view (l % l) (('x','y'),'z')
'x'Composing optics of different kinds is fine, provided they have a common supertype, which the composition returns:
*Optics> :type l % sets fmap
l % sets fmap
  :: Functor f => Optic A_Setter '[] (f a, b1) (f b2, b1) a b2However, some optic kinds do not have a common supertype, in which case a type error results from trying to compose them:
*Optics> :type to
to :: (s -> a) -> Getter s a
*Optics> :type to fst % sets fmap
<interactive>:1:1: error:
    • A_Getter cannot be composed with A_Setter
    • In the expression: to fst % sets fmapThe type of (%) itself is not entirely trivial. It relies on a type family
Join to calculate the least upper bound of a pair of optic kinds:
*Optics> :type (%)
(%)
  :: (Is k (Join k l), Is l (Join k l)) =>
     Optic k is s t u v
     -> Optic l js u v a b -> Optic (Join k l) (Append is js) s t a bHowever, you rarely work with (%) directly, and see only the results. The
Join type family can be evaluated directly to determine how two optic kinds
compose:
*Optics> :kind! Join A_Lens A_Setter
Join A_Lens A_Setter :: *
= A_Setter
*Optics> :kind! Join A_Getter A_Setter
Join A_Getter A_Setter :: *
= (TypeError ...)A little lens comparison
For comparison, let’s try the same sequence of commands with lens. Here the
underlying implementation using the van Laarhoven representation is rapidly
visible:
Control.Lens> :info Lens
type Lens s t a b =
  forall (f :: * -> *). Functor f => (a -> f b) -> s -> f t
Control.Lens> :type lens
lens
  :: Functor f => (s -> a) -> (s -> b -> t) -> (a -> f b) -> s -> f t
Control.Lens> let l = lens (\(x,_) -> x) (\(_,y) x -> (x,y))
l :: Functor f => (a1 -> f a2) -> (a1, b) -> f (a2, b)Using view and set is not much different:4
Control.Lens> :type view
view
  :: Control.Monad.Reader.Class.MonadReader s m =>
     Getting a s a -> m a
Control.Lens> :type set
set :: ASetter s t a b -> b -> s -> t
Control.Lens> view l ('a','b')
'a'
Control.Lens> set l 'c' ('a','b')
('c','b')However, attempting to use a Setter where a Getter is expected does not
report an error immediately, and when it does, the message is somewhat
inscrutable:
Control.Lens> :type sets
sets
  :: (Profunctor p, Profunctor q, Settable f) =>
     (p a b -> q s t) -> Optical p q f s t a b
Control.Lens> :type view (sets fmap)
view (sets fmap)
  :: (Control.Monad.Reader.Class.MonadReader (f b) m,
      Settable (Const b), Functor f) =>
     m b
Control.Lens> view (sets fmap) ('x','y')
<interactive>:82:7: error:
    • No instance for (Settable (Const Char))
        arising from a use of ‘sets’
...Somewhat magically, lens uses the (.) function composition operator for
optic composition:
Control.Lens> :type l . l
l . l
  :: Functor f => (a1 -> f a2) -> ((a1, b1), b2) -> f ((a2, b1), b2)
Control.Lens> view (l . l) (('x','y'),'z')
'x'Even more magically, this automatically selects the appropriate supertype when composing different optic kinds:
Control.Lens> :type l . sets fmap
l . sets fmap
  :: (Settable f1, Functor f2) =>
     (a -> f1 b1) -> (f2 a, b2) -> f1 (f2 b1, b2)Once more, however, illegitimate compositions are not detected immediately but lead to a type with class constraints that can never be usefully satisfied:
Control.Lens> :type to
to :: (Profunctor p, Contravariant f) => (s -> a) -> Optic' p f s a
Control.Lens> :type to fst . sets fmap
to fst . sets fmap
  :: (Contravariant f1, Settable f1, Functor f2) =>
     (b1 -> f1 b1) -> (f2 b1, b2) -> f1 (f2 b1, b2)Overloaded labels
Suppose we define two datatypes with the same field name:
data Human = Human { name :: String } deriving Show
data Pet = Pet { name :: String } deriving ShowNow we have a problem if we try to use name as a record selector or in a
record update, because it is ambiguous which datatype is meant. The
DuplicateRecordFields GHC extension can help with this to some extent, but it
makes very limited use of type information to resolve the ambiguity. For
example, name (Human "Peter" :: Human) will work but name (Human "Peter") is
still considered ambiguous.
The GHC OverloadedLabels extension is intended to help in this situation, by
providing a new syntax #name for an “overloaded label” whose interpretation is
determined by its type. In particular, we can use overloaded labels as optics by
giving instances of the LabelOptic class, with a few GHC extensions and a bit
of boilerplate:5
{-# LANGUAGE OverloadedLabels DataKinds FlexibleInstances MultiParamTypeClasses
             UndecidableInstances TypeFamilies #-}
instance (a ~ String, b ~ String) => LabelOptic "name" A_Lens Human Human a b where
  labelOptic = lens (\ (Human n) -> n) (\ _h n -> Human n )
instance (a ~ String, b ~ String) => LabelOptic "name" A_Lens Pet Pet a b where
  labelOptic = lens (\ (Pet n) -> n) ( \ _p n -> Pet n )Now we can use #name as a Lens, and the types will determine which field of
which record is intended:
*Optics> view #name (Human "Peter")
"Peter"
*Optics> set #name "Goldie" (Pet "Sparky")
Pet {name = "Goldie"}For more details on the support for overloaded labels in optics, check out the
Haddocks for
Optics.Label.
The hierarchy of optics
In optics, the hierarchy of optic kinds is closed, i.e. it is not possible to
discover and make use of new optic kinds without modifying the library. Our aim
is to make it easier to understand the interfaces and uses of different optic
kinds, but this comes at the cost of obscuring some of the underlying common
structure of the van Laarhoven or profunctor representations. One concrete
limitation relative to lens is that we have not yet explored support for
non-empty folds and traversals (Fold1 and Traversal1).
The diagram below shows the hierarchy of optic kinds supported by the initial
release. Each arrow points from a subtype to its immediate supertype, e.g. every
Lens can be used as a Getter:
 
The details of how indexed optics work are beyond the scope of this blog post
(see the indexed optics
Haddocks if
you are interested), but the diagram below shows that every optic above Lens
in the subtype hierarchy has an accompanying indexed variant:
 
Summary
What are the key ideas underpinning the optics library?
- Every optic kind has a clear separation between interface and implementation, with a - newtypeabstraction boundary. This means the types reflect concepts such as lenses directly, rather than encoding them using higher-rank polymorphism. This leads to good type inference behaviour and (hopefully) clear error messages.
- The interface of each optic kind is clearly and systematically documented. See the documentation for - Optics.Lensas an example.
- Since optics are not functions, they cannot be composed with the - (.)operator. Instead a separate composition operator- (%)is provided.
- Subtyping between different optic kinds (e.g. using a lens as a traversal) is accomplished using typeclasses. This is mostly automatic, although explicit casts are possible and occasionally necessary. 
- Optics work with the - OverloadedLabelsGHC extension to allow the same name to be used for fields in different datatypes.
- Under the hood, - opticsuses the indexed profunctor encoding (rather than the van Laarhoven encoding used by- lens). This allows us to support affine optics (which have at most one target). We provide conversions between the- opticsand- lensrepresentations; for isomorphisms and prisms these are in a separate package- optics-vlas this incurs a dependency on- profunctors.
- Indexed optics have a generally similar user experience to - lens, but with different ergonomics (e.g. all optics are index-preserving, and there is no separate- Conjoinedclass).
- The main - Opticsmodule exposes only a restricted selection of operators, making inevitably opinionated choices about which operators are the most generally useful.
- Sometimes functions in - opticshave a more specific type than the most general type possible, in the interests of simplicity and reducing the likelihood of errors. For example- viewdoes not work on folds, instead there is a separate function- foldOfto eliminate folds, or- gviewif you really want additional polymorphism.
- For library writers who wish to define optics as part of their library interface, we provide a cut-down - optics-corepackage with significant functionality but minimal dependencies (only GHC boot libraries). Unlike- lens, it is not possible to define lenses without depending on at least- optics-core.
For a full introduction to optics, check out the Haddocks for the main Optics
module. We
welcome feedback and contributions on the GitHub well-typed/optics
repo.
Acknowledgements
I would like to thank my coauthors Andrzej Rybczak, Oleg Grenrus and Andres Löh
for all their work on optics. Edsko de Vries, Alp Mestanogullari, Ömer Sinan
Ağacan and other colleagues at Well-Typed gave helpful feedback on the library
in general and this blog post in particular. Thanks are also due to Edward Kmett
for his work on lens and for critiquing (though not necessarily endorsing!)
the ideas behind this library.
- They are also polymorphic in - is, so they can be used with both indexed and unindexed optics.↩︎
- Neither do optics form a - Category, because this would rule out optics with type-changing update or composition of optics of different kinds.↩︎
- An implementation detail leaks through here: the empty list - '[]corresponds to- NoIxand represents the empty list of indices, meaning that this optic is not indexed.↩︎
- lensgeneralises- viewover any- MonadReader, and permits it to work on folds, whereas- opticschooses not to by default. We provide a- gviewfunction in- Optics.Viewthat can be used similarly to- viewfrom- lens.↩︎
- The boilerplate can be generated by Template Haskell now, and we are exploring making use of - Genericinstead. In the future we may be able to use a planned but not-yet-implemented addition to the GHC- HasFieldclass.↩︎